JavaからScala、そしてClojureへ 実務で活きる関数型プログラミング
lagénorhynque 🐬カマイルカ (defprofile lagénorhynque :id @lagenorhynque :readings ["/laʒenɔʁɛ̃ k/" "ラジェノランク"] :aliases ["カマイルカ" "🐬"] :languages [Java Japanese ; native languages Clojure Haskell ; functional languages English français] ; European languages :interests [programming language-learning law politics mathematics])
1. 私と仙台 2. 私と関数型言語 3. Opt Technologiesと関数型言語 4. 関数型プログラミングの実践例 5. 関数型プログラミング的な発想
私と仙台
プライベート 岐阜出身 2012年春〜: 東京 2022年春〜: 千葉 仙台/宮城/東北に接点は(たぶん)なさそう が以前から気になっている🐬 仙台うみの杜水族館
仕事(オプト) オプトの は主に仙台拠点で 開発運用されていた 2017年頃から東京の開発部門も関わり始める そのタイミングで東京所属の私もジョイン 2018年には仙台拠点へ出張する機会も 2021年にフルリモートワーク前提で東京と仙台 の開発部門が統合された 現在も仙台在住のメンバーと日常的に一緒に仕事 している 広告運用支援ツール群
私と関数型言語
year event 2011 大学(法学部) 4年で初めてプログラミングに 少し触れる: SQL, Java 2012 前職の会社に新卒入社し、 Javaでの業務システム開発に携わり始める 2014 趣味で関数型言語に傾倒する: Haskell, Clojure, Erlang, Scala, OCaml, etc. 2015 Clojure, Haskell, Scalaの勉強会に参加する ようになり、のちの同僚とも出会う
year event 2016 オプトに中途入社し、 大規模なScala開発を経験する 2017 開発中のプロダクトの小さなバッチに Clojureを社内初導入する 2018 新規プロダクトのREST API実装にClojureを 採用する 2019 新規プロダクトのGraphQL API実装に Clojureを採用する 2021 開発チームを離れ、開発組織横断的な技術 マネジメント業務へ
発表: at on 2017/01/28 JavaプログラマこそClojureを始めようという悪魔 の誘い😈 JavaからClojureへ 第十八回#渋谷java
発表: at on 2019/07/25 オプトでのClojure採用から普及の歴史⚔️について ジョーク成分多めに紹介 Clojurian Conquest Shibuya.lisp lispmeetup #78
記事: Clojureをプロダクトに導入した話
記事: サービス間連携のためのGraphQL APIをClojure で開発している話
Opt Technologiesと関数型言語
社内での関数型言語利用の歴史
発足(2016年)以前から前身とな った開発会社でメイン開発言語だった 近年はバックエンド開発の利用言語が多様化して いるが、引き続き主要言語のひとつ 2017年の導入からシェアが拡大し、重要なプロ ダクトを支える言語のひとつになった 当初は一人しか経験者がいなかったが、継続的に 開発可能な体制に成熟してきた Scala Opt Technologies Clojure
2018年頃に導入を試みたが、プロダクト開発が 諸事情により中止になり現存しない😇 開発者向け管理画面のために小さく使われている 例がある 上記HaskellプロダクトのWebフロントエンドに も採用されていた cf. Haskell Elm Opt Technologiesの主な利用技術
関数型プログラミングの実践例
Java オブジェクト指向・非関数型言語 静的型付き言語 JVM言語 関数型プログラミング関連機能が充実してきた cf. // メソッドの定義 jshell> void hello(Object x) { ...> System.out.println("Hello, %s!".formatted(x)); ...> } | created method hello(Object) // メソッドの呼び出し jshell> hello("Java") Hello, Java! jshell コマンド
Scala オブジェクト指向・関数型言語 静的型付き言語 JVM言語 オブジェクト指向に関数型が溶け込んだ言語 cf. // メソッドの定義 scala> def hello(x: Any): Unit = | println(s"Hello, $x!") | def hello(x: Any): Unit // メソッドの呼び出し scala> hello("Scala") Hello, Scala! scala コマンド
Clojure 非オブジェクト指向・関数型言語 動的型付き言語 JVM言語 オブジェクト指向を嫌い関数型を志向したLisp cf. + ;; 関数の定義 user=> (defn hello [x] #_=> (println (str "Hello, " x "!"))) #'user/hello ;; 関数の呼び出し(適用) user=> (hello "Clojure") Hello, Clojure! nil clojure コマンド rebel-readline
とあるプロダクトのJavaコード(抜粋) return mediaProcessLogDao.selectMediaProcessLogs(baseDate, modifiedEntities).stream() .map(this::normalizeTargetIndexIfAdvertise) .collect(Collectors.groupingBy(MediaProcessLogEntity::getKey)) .entrySet().stream().collect(toMap( Map.Entry::getKey, group -> { List<MediaProcessLogEntity> entities = group.getValue(); if (entities.stream() .allMatch(MediaProcessLogEntity::isEmpty)) { return true; } return entities.stream() .filter(e -> !e.isEmpty()) .allMatch(MediaProcessLogEntity::isImported); }));
問題を単純化すると エンティティのリストをその要素のキーごとにグルー ピングし、個々のグループの値が特定の条件を満たす かどうかを表す対応表(マップ)がほしい。 どのようなプログラムに落とし込む?
Java: サンプルデータ jshell> record Entity(int key, String x) {} | created record Entity jshell> final var entities = List.of( ...> new Entity(3, "a"), ...> new Entity(1, "b"), ...> new Entity(2, "c"), ...> new Entity(1, "d"), ...> new Entity(1, "e") ...> ) entities ==> [Entity[key=3, x=a], Entity[key=1, x=b], Entity[k ... x=d], Entity[key=1, x=e]]
Java: 命令型(imperative)のアプローチ jshell> final var keyToEntities = ...> new HashMap<Integer, List<Entity>>(); ...> for (final var e : entities) { ...> final var es = keyToEntities.getOrDefault(e.key(), ...> new ArrayList<Entity>()); ...> es.add(e); ...> keyToEntities.put(e.key(), es); ...> } keyToEntities ==> {} jshell> keyToEntities keyToEntities ==> {1=[Entity[key=1, x=b], Entity[key=1, x=d], Entity[key=1, x=e]], 2=[Entity[key=2, x=c]], 3=[Entity[key=3, x=a]]}
jshell> final var result = new HashMap<Integer, Boolean>(); ...> for (final var entry : keyToEntities.entrySet()) { ...> result.put(entry.getKey(), ...> entry.getValue().size() > 1); ...> } result ==> {} jshell> result result ==> {1=true, 2=false, 3=false}
Java: 関数型(functional)のアプローチ ※ REPLでの行継続のため行末に. を置いている jshell> entities.stream(). ...> collect(Collectors.groupingBy(Entity::key)). ...> entrySet().stream(). ...> collect(Collectors.toMap( ...> Map.Entry::getKey, ...> group -> group.getValue().size() > 1 ...> )) $3 ==> {1=true, 2=false, 3=false}
Scala: サンプルデータ scala> case class Entity(key: Int, x: String) // defined case class Entity scala> val entities = Seq( | Entity(3, "a"), | Entity(1, "b"), | Entity(2, "c"), | Entity(1, "d"), | Entity(1, "e"), | ) val entities: Seq[Entity] = List(Entity(3,a), Entity(1,b), Entity(2,c), Entity(1,d), Entity(1,e))
Scala: 関数型(functional)のアプローチ scala> entities. | groupBy(_.key). | view. | mapValues(_.length > 1). | toMap val res0: Map[Int, Boolean] = Map(1 -> true, 2 -> false, 3 -> false)
Clojure: サンプルデータ user=> (def entities [#:entity{:key 3 #_=> :x "a"} #_=> #:entity{:key 1 #_=> :x "b"} #_=> #:entity{:key 2 #_=> :x "c"} #_=> #:entity{:key 1 #_=> :x "d"} #_=> #:entity{:key 1 #_=> :x "e"}]) #'user/entities
Clojure: 関数型(functional)のアプローチ user=> (update-vals (group-by :entity/key entities) #_=> #(> (count %) 1)) {3 false, 1 true, 2 false}
考察: 命令型(imperative)のアプローチ 2種類の変数とfor文によるループ処理 変数やメソッドの命名、レイアウトなどの工夫を しないとコードの意図が埋もれがち 文(statement)が登場し、命令(コマンド)の並びとし て表現されている マップやリストが破壊的に更新されている: 可変 (mutable)データ 変数への再代入を封じる(Javaではfinal を付ける) だけでも安心感が高まる
考察: 関数型(functional)のアプローチ リストをグルーピングし、マップの値を変換すると いう意図が関数/メソッドで表されている 引数で振る舞いを指定している: 高階関数 (higher-order function) 与えているのは無名関数(anonymous function)/ ラムダ式(lambda expression) cf. オブジェクト指向のStrategyパターン 関数型言語では汎用的で高機能な関数/メソッド が標準で充実している
全体が式(expression)で構成され、データの変換と して表現されている 今回の例では途中過程にローカル変数もない 関数型言語では簡潔に関数を組み合わせ加工する 手段が豊富に用意されている コード上で更新される変数やデータは見当たらない 調べてみると初期化以降にデータが更新されてい ないことが分かる: 不変(immutable)データ 関数型言語ではデフォルトで変数は再代入でき ず、不変データを利用しやすくなっていることが 多い
関数型プログラミング的な発想
「イミュータビリティ」と「コンポーザビリティ」を 重視する
イミュータビリティ(immutability; 不変性) 形容詞形: イミュータブル(immutable; 不変) 対義語: ミュータビリティ(mutability; 可変性)、 ミュータブル(mutable; 可変) もとのまま変化しない(させられない)性質 凍結するイメージ? 🧊 破壊的な更新操作(再代入、更新、削除)を提供せ ず、作成(初期化)し、取得する(読み取る)ことに 徹する
主なメリット 可読性や変更容易性、コンポーザビリティが向上 しやすくなる デバッグやテストも容易になる 並行プログラミング、分散システムと相性が良い プログラミング言語に限らない例 イミュータブルインフラストラクチャ 台帳データベース、追記型のRDBテーブル設計 バージョン管理システム
コンポーザビリティ(composability; 合成可能性) 形容詞: コンポーザブル(composable; 合成可能) 要素同士が組み合わせられる性質 LEGOブロックのイメージ? 🧱
主なメリット 再利用性や拡張性が向上する 高凝集で疎結合な「モジュール」(ソフトウェア コンポーネント)に繋がる プログラミング言語に限らない例 Pipes & Filters Ports & Adapters (ヘキサゴナルアーキテクチャ) Single Responsibility Principle (SRP) ( によるプレゼン) UNIX哲学 Simple Made Easy Rich Hickey
関数型プログラミングは楽しい😆 関数型言語は怖くない( JavaプログラマこそScalaや Clojureを試してみよう! )。 思考のリソースを節約し、扱いやすいソフトウェアを 設計するために、その発想を活かそう。
Further Reading
コミュニティイベント : Haskell : Lisp系言語(Clojure, Common Lispな ど) : Elixir : Scala Haskell-jp Shibuya.lisp fukuoka.ex/kokura.ex/ElixirImp rpscala
書籍 : Scala, Erlang, Clojure, Haskell cf. (原書続 編): Elixir, Elm, Idris Scala 『7つの言語7つの世界』 Seven More Languages in Seven Weeks 『実践Scala入門』 『Scalaスケーラブルプログラミング第4版』 『Scala関数型デザイン&プログラミング』
Clojure cf. (原書第3 版) Haskell 『プログラミングClojure 第2版』 Programming Clojure, Third Edition Getting Clojure Clojure Applied 『[増補改訂]関数プログラミング実践入門』 『プログラミングHaskell 第2版』 『すごいHaskellたのしく学ぼう!』 『Haskell入門関数型プログラミング言語の基礎と 実践』
OCaml Erlang Elixir 『プログラミングの基礎』 『プログラミングin OCaml』 『プログラミングErlang』 『すごいErlangゆかいに学ぼう!』 『プログラミングElixir(第2版)』

JavaからScala、そしてClojureへ: 実務で活きる関数型プログラミング