36.9K Views
June 24, 22
スライド概要
JavaプログラマこそScalaやClojureを試してみよう!
「楽しく楽にcoolにsmartに」を理想とするprogrammer/philosopher/liberalist/realist。 好きな言語はClojure, Haskell, Python, English, français, русский。 読書、プログラミング、語学、法学、数学が大好き! イルカと海も大好き🐬
からScala、そしてClojureへ Java 実務で活きる関数型プログラミング
lagénorhynque 🐬 カマイルカ (defprofile lagénorhynque :id @lagenorhynque :readings ["/laʒenɔʁɛ̃ k/" " :aliases [" " " "] カマイルカ 🐬 ラジェノランク"] :languages [Java Japanese Clojure Haskell English français] :interests [programming language-learning law politics mathematics]) ; native languages ; functional languages ; European languages
私と仙台 私と関数型言語 1. 2. 3. Opt Technologies 4. 5. と関数型言語 関数型プログラミングの実践例 関数型プログラミング的な発想
私と仙台
プライベート 岐阜出身 2012年春〜: 東京 2022年春〜: 千葉 仙台/宮城/東北に接点は(たぶん)なさそう 仙台うみの杜水族館が以前から気になっている🐬
仕事(オプト) オプトの広告運用支援ツール群は主に仙台拠点で 開発運用されていた 2017年頃から東京の開発部門も関わり始める そのタイミングで東京所属の私もジョイン 2018年には仙台拠点へ出張する機会も 2021年にフルリモートワーク前提で東京と仙台 の開発部門が統合された 現在も仙台在住のメンバーと日常的に一緒に仕事 している
私と関数型言語
year 2011 2012 2014 event 大学(法学部) 4年で初めてプログラミングに 少し触れる: SQL, Java 前職の会社に新卒入社し、 Javaでの業務システム開発に携わり始める 趣味で関数型言語に傾倒する: Haskell, Clojure, Erlang, Scala, OCaml, etc. 2015 Clojure, Haskell, Scala の勉強会に参加する ようになり、のちの同僚とも出会う
year 2016 2017 2018 2019 2021 event オプトに中途入社し、 大規模なScala開発を経験する 開発中のプロダクトの小さなバッチに Clojureを社内初導入する 新規プロダクトのREST API実装にClojureを 採用する 新規プロダクトのGraphQL API実装に Clojureを採用する 開発チームを離れ、開発組織横断的な技術 マネジメント業務へ
発表: JavaからClojureへ 第十八回 #渋谷java on 2017/01/28 プログラマこそClojureを始めようという悪魔 の誘い😈 at Java
発表: Clojurian Conquest at Shibuya.lisp lispmeetup #78 on 2019/07/25 Clojure オプトでの 採用から普及の歴史⚔️について ジョーク成分多めに紹介
記事: Clojureをプロダクトに導入した話
記事: サービス間連携のためのGraphQL APIをClojure で開発している話
と関数型言語 Opt Technologies
社内での関数型言語利用の歴史
Scala Opt Technologies 発足(2016年)以前から前身とな った開発会社でメイン開発言語だった 近年はバックエンド開発の利用言語が多様化して いるが、引き続き主要言語のひとつ Clojure 2017 年の導入からシェアが拡大し、重要なプロ ダクトを支える言語のひとつになった 当初は一人しか経験者がいなかったが、継続的に 開発可能な体制に成熟してきた
Haskell 2018 年頃に導入を試みたが、プロダクト開発が 諸事情により中止になり現存しない😇 Elm 開発者向け管理画面のために小さく使われている 例がある 上記HaskellプロダクトのWebフロントエンドに も採用されていた cf. Opt Technologiesの主な利用技術
関数型プログラミングの実践例
Java
オブジェクト指向・非関数型言語
静的型付き言語
JVM言語
関数型プログラミング関連機能が充実してきた
メソッドの定義
//
jshell> void hello(Object x) {
...>
System.out.println("Hello, %s!".formatted(x));
...> }
| created method hello(Object)
メソッドの呼び出し
//
jshell> hello("Java")
Hello, Java!
cf. jshell
コマンド
Scala オブジェクト指向・関数型言語 静的型付き言語 JVM言語 オブジェクト指向に関数型が溶け込んだ言語 メソッドの定義 // scala> def hello(x: Any): Unit = | println(s"Hello, $x!") | def hello(x: Any): Unit メソッドの呼び出し // scala> hello("Scala") Hello, Scala! cf. scala コマンド
Clojure
非オブジェクト指向・関数型言語
動的型付き言語
JVM言語
オブジェクト指向を嫌い関数型を志向したLisp
関数の定義
;;
user=> (defn hello [x]
#_=>
(println (str "Hello, " x "!")))
#'user/hello
関数の呼び出し 適用
;;
(
)
user=> (hello "Clojure")
Hello, Clojure!
nil
cf. 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)のアプローチ
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}
での行継続のため行末に . を置いている
※ REPL
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 ( UNIX Single Responsibility Principle (SRP) Simple Made Easy (Rich Hickey 哲学 ヘキサゴナルアーキテクチャ) によるプレゼン)
関数型プログラミングは楽しい😆 関数型言語は怖くない( JavaプログラマこそScalaや Clojureを試してみよう! )。 思考のリソースを節約し、扱いやすいソフトウェアを 設計するために、その発想を活かそう。
Further Reading
コミュニティイベント Haskell-jp: Haskell Shibuya.lisp: Lisp (Clojure, Common Lisp ) fukuoka.ex/kokura.ex/ElixirImp: Elixir rpscala: Scala ど 系言語 な
書籍 『7つの言語 7つの世界』: Scala, Erlang, Clojure, Haskell cf. Seven More Languages in Seven Weeks (原書続 編): Elixir, Elm, Idris Scala 『実践Scala入門』 『Scalaスケーラブルプログラミング 第4版』 『Scala関数型デザイン&プログラミング』
Clojure 『プログラミングClojure 第2版』 cf. Programming Clojure, Third Edition (原書第3 版) Getting Clojure Clojure Applied Haskell 『[増補改訂]関数プログラミング実践入門』 『プログラミングHaskell 第2版』 『すごいHaskellたのしく学ぼう!』 『Haskell入門 関数型プログラミング言語の基礎と 実践』
OCaml 『プログラミングの基礎』 『プログラミング in OCaml』 Erlang 『プログラミングErlang』 『すごいErlangゆかいに学ぼう!』 Elixir 『プログラミングElixir(第2版)』