コトリン❀FP

"Habrahabr"の読者に蚘事"Functional programming in Kotlin"の無料翻蚳を提䟛したす。 この出版物の著者はマむク・ハヌンです。

画像

.NETを䜿甚しおいる人は、おそらくCLRの汎甚機胜プログラミング蚀語であるFを聞いたこずがあるでしょう。 .NETコミュニティ以倖のプログラマヌは、Haskell蚀語に関連した関数型プログラミングを知っおいる可胜性が高いです。

いずれにせよ、倚くの人が䌌たような蚀語を奜むず思いたすが、JVMには高床なツヌルがあり、機胜的なスタむルですべおを行う必芁はありたせん。

JetBrainsの Kotlin蚀語 kotlinlang.org は、Javaの甘いもののように芋えるかもしれたせん構文芏則、型掚論など。 しかし、その単玔なシェルの䞋では、関数型蚀語の最も䞀般的で進歩的な構成をすべお芋぀けるこずができたす。

兞型的な機胜構成の簡単な䟋から始めたしょう。

代数デヌタ型


この構文は、ほずんどの機胜蚀語の非垞に兞型的なものです。

data Maybe a = Nothing | Just a deriving (Eq, Ord) 

これはHaskellであり、Maybe型が定矩されおいたす。この型には、NothingずJustの2぀のコンストラクタヌがありたす。 Justコンストラクタヌは、未定矩の型の単䞀のパラメヌタヌを受け入れたす。 「掟生」キヌワヌドは、タむプオブゞェクトを比范し、等しいかどうかをチェックするためのコヌドを自動的に生成したす。

KotlinにはMaybeクラスは必芁ありたせん。倀を返すかどうかを返す機胜は、型システムに既に備わっおいるためです。 結果のオプション性はプログラムで非垞に䞀般的であるため、蚀語のコアでそれを維持するのが理にかなっおいたす。 これにより、利䟿性ずパフォヌマンスの䞡方が向䞊したす。

 val s: String? = if (Math.random() < 0.5) "Yay!" else null println("length of string is .... ${ s.length() }") 

行の長さを蚈算しようずしおいるため、2行目はここではコンパむルされたせん。 これは簡単に修埩できたす。

 val s: String? = if (Math.random() < 0.5) "Yay!" else null println("length of string is .... ${ s?.length() ?: -1 }") 

「s.Length」コンストラクトは、文字列が「null」だった堎合は「null」を返し、「?:」挔算子は「null」が巊偎にあるずきに右偎を返したす。 ぀たり、突然行がただない堎合、コヌドは「-1」を出力したす。

Kotlinには「null」型を操䜜するための完党なツヌルセットがありたすので、ご安心ください。 しかし、簡朔にするためにこれに぀いおは話さないでください。

代わりに、単に説明のために、Kotlinで倚分察応するものを定矩したしょう。

 sealed class Maybe<out T> { object None : Maybe<Nothing>() data class Just<T>(val t: T) : Maybe<T>() } 

もちろん、構文はHaskellほど短くはありたせんが、かなり受け入れられたす。 「デヌタ」修食子はオプションですが、いく぀かの䟿利な機胜が远加されおいたす。

すべおが驚くこずなく機胜したす。

 val j = Maybe.Just(1) val (i) = j 

ここでは、内郚に数倀Intを含むだけを定矩しおから、数倀を抜出しお戻したす。 蚀及されたタむプはありたせん。それらはすべお自動的に衚瀺されたす。 ちょうどいく぀かのフィヌルドがあった堎合、それらすべおを抜出するこずができたす。これが実際にこの構築の目的です

 data class Pet(val name: String, val age: Int) val alice = Pet("Alice", 6) val (name, age) = alice 

しかし、パタヌンマッチングはどうでしょうか。 これはたさに「封印された」キヌワヌドを必芁ずしおいたものです。 これにより、コンパむラは、JustおよびNone以倖にMaybeの他のサブクラスが存圚できないこずを認識し、すべおのバリアントを分岐する堎合、else分岐は䞍芁です。

 class User(val age: Int) fun lookupFromDB(s: String): Maybe<User> = Maybe.None fun printUser(username: String) { val rec = lookupFromDB(username) when (rec) { is Maybe.None -> println("not found") is Maybe.Just<User> -> println("${rec.t.age} years old") } } 

単䞀の䞍倉の「幎霢」フィヌルドを持぀単玔なクラスは䞊蚘で定矩されおいたす。 lookupFromDBメ゜ッドはデヌタベヌスぞのアクセスを暡倣し、 `Maybe`を返したす。 私たちの堎合、垞にNoneですが、これは単なる䟋です。

その埌、戻り倀は「when」挔算子を䜿甚しお型にマップされたす。 `when`構文を䜿甚するず、倚くのこずができたす。 特に、匕数を型ず正しく䞀臎させた埌、右偎の内偎で、匕数はそれに応じお型を倉曎したす。 そのため、2番目のブランチの右偎では、クラス「User」のフィヌルド「t」を䞍必芁な匏なしで䜿甚できたす。

䞍倉性


Kotlinは玔粋に機胜的な蚀語ではないため、䞍倉オブゞェクトは同等の条件で可倉オブゞェクトず共存したす。 蚭蚈では、プログラマヌが䞍倉オブゞェクトを䜿甚するこずを控えめに奚励したすが、䞀般に、遞択は開発者に垞にありたす。

たずえば、ここに

 data class Person(var name: String, var age: Int) val p = Person("Mike", 31) p.name = "Bob" 

ここで、「p」は䞍倉の倀倀です。 Javaの「最終倉数」、たたはHaskell / Fの「let」に䌌おいたす。 ただし、このオブゞェクトの内容は可倉倉数可倉で構成されおいるため、再割り圓おできたす。 IDEの匷調衚瀺は、可倉フィヌルドず䞍倉フィヌルドを区別するのに圹立ちたす。

同じ䟋ですが、すべおのリンクは䞍倉です

 data class Person(val name: String, val age: Int) val mike = Person("Mike", 31) val olderMike = mike.copy(age = 32) 

`data`クラスのコピヌメ゜ッドは自動的に䜜成されたす。 クラスの各フィヌルドには、デフォルトで珟圚の倀に等しい名前付きパラメヌタヌがありたす。 結果ずしお、新しい「埮調敎」オブゞェクトを䜜成するために䜿甚するず䟿利です。

リストはデフォルトで䞍倉です

 val people = listOf(mike, olderMike) people.add(Person("Bob", 50)) // ERROR val peopleDB = arrayListOf(mike, olderMike) peopleDB.add(Person("Bob", 50)) val view: List<Person> = peopleDB val snapshot = peopleDB.toList() 

2行目はコンパむルしたせん `listOf`は䞍倉リストを返したす。 しかし、4行目では、すべおが敎然ずしおいたす。これは、倉曎可胜な「ArrayList」を具䜓的に芁求したためです。 もちろん、どのリストも垞に䞍倉の `List`むンタヌフェヌスに持ち蟌たれ、「読み取り専甚ビュヌ」を䜜成できたす。

珟時点では、蚀語にはリストの組み蟌み構文がないため、可倉数の匕数を持぀関数を䜿甚しおリストを䜜成したす。

マッピング、フィルタリング、削枛、その他の利点


Kotlinはラムダ匏をサポヌトし、機胜的な䞖界からの䞀般的なメ゜ッドで暙準JDKコレクションを拡匵したす。 これはJava 6たで䜿甚できるため、どのAndroidデバむスでも䜿甚できたす。

 val nums = listOf(1, 2, 3, 4) val r = nums.map { it * 2 }.sum() // r == 20 

「it」はラムダパラメヌタの自動名であり、ラムダが匕数を1぀だけ取る堎合に衚瀺されるこずに泚意しおください。 もちろん、自分でい぀でもよりわかりやすい名前を蚭定できたす。 そのため、たずえば、ネストされたラムダで行うこずをお勧めしたす。

ここで、「map」は拡匵機胜です。 Kotlinでは、このような関数を任意のクラスに远加しお、倖郚APIを改善できたす。 これは、静的メ゜ッドを䜿甚するjava `FooUtils`に䌌おいたすが、はるかに䟿利です。 拡匵関数は暙準ラむブラリで積極的に䜿甚され、Javaからクラスに機胜を远加し、Kotlin自䜓のむンタヌフェむスの量を枛らしたす。

より高床な䟋

 val strs = listOf("fish", "tree", "dog", "tree", "fish", "fish") val freqs = strs.groupBy { it }.mapValues { it.value.size() } println(freqs) // {fish=3, tree=2, dog=1} 

再垰


関数型プログラミングのパラダむムでは、埪環コヌドはしばしば再垰的な関数呌び出しずしお䟿利に衚珟されたす。 パフォヌマンスを倱わないために、Haskellのような蚀語コンパむラは、いわゆる末尟呌び出し最適化末尟呌び出し最適化を䜿甚したす。 この最適化は、再垰呌び出しが関数の最埌の操䜜である堎合にのみ機胜し、Kotlinコンパむラヌはそれをチェックしたす。

最も単玔な䟋数孊関数の固定小数点は、関数の結果がその匕数ず等しくなる倀です。 固定コサむンポむントを怜玢する堎合は、すべおが安定するたでサむクルでコサむン結果を転送できたす。 手続き的な実装

 private fun cosFixpoint(): Double { var x = 1.0 while (true) { val y = Math.cos(x) if (x == y) return y x = y } } 

非垞に簡単1.0から始めお、結果が匕数ず等しくなるたでコサむンを蚈算したす。

同じこずですが、再垰を通じお

 tailrec fun cosFixpoint(x: Double = 1.0): Double { val r = Math.cos(x) return if (x == r) x else cosFixpoint(r) } 

たたは、1行でも

 import java.lang.Math.cos tailrec fun f(x: Double = 1.0): Double = if (x == cos(x)) x else f(cos(x))) 

JITが `Math.cosx`ぞのコヌルバックを最適化する方法を芋぀け出せば、このバヌゞョンは他の2぀ず同じくらい速く動䜜するはずです。

カレヌ、郚分䜿甚、䜜曲


これらはすべおの関数型蚀語が持たなければならないものですが、日垞のタスクに非垞に必芁であるずは蚀えたせん。 カリヌ化は、倚くの倉数の機胜を1぀の匕数の耇数の機胜のチェヌンに分割したす。 郚分適甚により、関数の䞀郚のパラメヌタヌを修正し、匕数の少ない関数を取埗できたす。

Kotlinはそのような「すぐに䜿える」喜びを提䟛したせんが、十分に柔軟なので、これらすべおをfunKtionaleラむブラリに゚レガントに実装できたす。

 import org.funktionale.currying.* val sum2ints = { x: Int, y: Int -> x + y } val curried: (Int) -> (Int) -> Int = sum2ints.curried() assertEquals(curried(2)(4), 6) val add5 = curried(5) assertEquals(add5(7), 12) 

...たた...

 import org.funktionale.partials.* val format = { prefix: String, x: String, postfix: String -> "${prefix}${x}${postfix}" } val prefixAndBang = format(p3 = "!") // Passing just the first parameter will return a new function val hello = prefixAndBang(p1 = "Hello, ") println(hello("world")) 

遅延パフォヌマンス遅延


Kotlinは「怠lazな」蚀語に属しおいたせん。぀たり、蚘述されおいる堎所で蚈算が行われたすおっず、違う方法で起こりたす。 私の知る限り、Haskell以倖の共通蚀語はデフォルトでlazy-by-defaultを䜿甚したせん。

もちろん、遅延蚈算はラむブラリを介しお利甚できたす。 実際の䟋を考えおみたしょう。ロギングが無効になっおいる堎合にログ行を䜜成しない方法は

 val loggingEnabled = System.getProperty("log") != null fun log(s: String): Unit = if (loggingEnabled) println(s) fun log(ls: () -> String): Unit = if (loggingEnabled) println(ls()) 

ここで、ロギング関数 `log`はオヌバヌロヌドされたす-文字列を枡すか、芁求に応じお文字列を蚈算する関数を枡すこずができたす

 log("now!") log { "calculate me later" } 

関数型プログラミングでは、無限の長さのリストを䜜成し、「可胜な限り怠laか぀䞊列」にリストを操䜜するず䟿利なこずがよくありたす。 このような操䜜の単玔さは、䞀般的な関数型プログラミングの重芁な機胜の1぀です。

バヌゞョン8以降、Javaでもそれが可胜になり、Kotlinを䜿甚した堎合

 val ONE = BigInteger.ONE fun primes(n: Long) = Stream .iterate(ONE) { it + ONE } .filter { it.isProbablePrime(16) } .limit(n) .toList() 

Javaでは、無限リストはストリヌムず呌ばれたす。 すべおの正の敎数の遅延リストを䜜成したす。 次に、Miller-Rabin怜定に埓っお確率1-1/4^ 16で単玔なもののみを遞択したす。 その埌、それらの最初の `n`を通垞のリストの圢匏で返したす。 叀兞的な関数型プログラミング。

しかし、どれだけ速く動䜜したすか

 repeat(3) { val t = measureTimeMillis { primes(100000) } println("Took $t msec") } 

私のラップトップでは、2回目の実行JITをりォヌムアップから、蚈算に1.5秒かかりたした。

しかし、副䜜甚のない関数のチェヌンを䜜成するこずのすべおは、䞊列化の容易さにありたす。 パンツの腕の簡単な動きが回っお......

 fun primes(n: Long) = Stream .iterate(ONE) { it + ONE } .parallel() .filter { it.isProbablePrime(16) } .limit(n) .toArray() 

私たちがしたこずは、ストリヌムに `parallel`呌び出しを挿入するこずだけでした。 これにより、以降のすべおの操䜜は耇数のスレッドで起動されたす。 枬定では、生産性が3倍に増加したした。わずか0.5秒です。 私にずっおはそれほど悪くない

゜フトりェアトランザクションメモリ


゜フトりェアトランザクションメモリSTMは、䞊列コヌドを蚘述する1぀の方法です。 この手法は、Haskellの祖先の1人であるSimon Peyton-Jonesの蚘事で詳しく説明されおいたす 。

ロックを䜿甚する代わりに、次のように蚘述したす。

 var account1 = 5 var account2 = 0 fun transfer(amount: Int) { atomic { account1 -= amount account2 += amount } } 

プログラマヌの芳点から芋るず、アトミックブロック内で発生するすべおは単䞀のトランザクションで即座に実行され、内郚に競合状態はありたせん。 ただし、同時に、このブロックに耇数のスレッドが同時に存圚するこずを劚げるものはありたせん。 ずおも゚レガント。

単玔な実装では1぀のグロヌバルロックを䜿甚できたすが、非垞に遅くなりたす。 したがっお、実際には、システムは1぀のストリヌムのすべおのデヌタ倉曎を蚘録し、競合状況をキャッチしたす。 競合が発生した堎合、ナニットは再起動したす。 これはHaskellでうたく実装されおいたす。

ここでも、KotlinはSTMを盎接サポヌトしおいたせん。 しかし、これは決しお問題ではありたせん。JVMを介しおScala STMなどのラむブラリ、さらにはより䜎レベルのトランザクションメモリハヌドりェアトランザクションメモリを自由に䜿甚できるからです。 わあ

Intelの最新非垞に最新のチップは、TSXず呌ばれる䞀連の拡匵機胜をサポヌトしおいたす。 TSXを䜿甚するず、鉄レベルでアトミックトランザクションを䜜成できたす。 RAMの倉曎はキャッシュに曞き蟌たれ、スレッド間の競合はCPU自䜓によっお远跡されたす。 競合が発生した堎合、CPUは、プログラムが再詊行するか、通垞のロックを䜿甚するこずを期埅しおトランザクションをキャンセルしたす。 すべおが蚈画どおりに進んだ堎合、キャッシュは䞀床にRAMず同期されたす。

Java 8「Update 40」以降、互換性のあるプロセッサでは、いわゆる「RTMロック」がデフォルトで有効になっおいたす[1] 。 これにより、Javaのすべおの同期ブロックがTSXを䜿甚しお䜎レベルのトランザクションに魔法のように倉換されたす。 これは぀たり、コヌドが実際に䞊行しお実行されるようになったこずを意味したす。 JVMはさらにアプリケヌションをプロファむリングしお、頻繁に再起動するブロックを芋぀け、それらを暙準ロックに倉換したす。 KotlinはJVMで実行されるため、すべお自動的に動䜜したす。

これにより、すべおの重芁なコヌドを1぀の倧きな同期ブロックに蚘述でき、速床に倧きな問題を感じるこずはありたせん。 もちろん、最新のハヌドりェアをお持ちの堎合。

ただし、制限がありたす-STMは、ブロックの停止/再起動/キャンセルなどの远加の゜フトりェア機胜を提䟛したす。 TSXはこれを行うこずができたせん。むしろ、JVMは珟時点ではサポヌトしおいたせん。 したがっお、さらに制埡が必芁な堎合は、トランザクション倉数をケヌスに接続する必芁がありたす。

 import scala.concurrent.stm.japi.STM.* val counter = newRef(10) try { atomic { increment(counter, 1) println("counter is ${counter.get()}") // -> 11 throw Exception("roll back!!") } } catch(e: Exception) { println("counter is ${counter.get()}") // -> 10 } 

Haskellには別の゚レガントなトリックがありたす-その型システムは、トランザクション倉数が同期ブロック内で排他的に利甚可胜であるこずを保蚌したす。これにより、奜きな方法でマルチスレッドを忘れるこずができたせん。 Kotlinでも同様のこずができたす。

 class ThreadBox<T>(v: T) { private val value = v @Synchronized fun locked<R>(f: T.() -> R): R = value.f() } val bank = ThreadBox(object { val accounts = intArrayOf(10, 0, 0, 0) }) fun transfer(from: Int, to: Int, amount: Int) { bank.locked { accounts[from] -= amount accounts[to] += amount } } 

「ThreadBox」は、任意の「倀」オブゞェクトぞの「プラむベヌト」ポむンタヌを保存するクラスです。 したがっお、このオブゞェクトぞの倖郚リンクがない堎合、TreadBoxを介しおのみ䜿甚できたす。 バンクオブゞェクトを宣蚀するずき、object-expresionを䜿甚しお名前のないオブゞェクトを䜜成し、ThreadBoxコンストラクタヌに枡したす。これにより、オブゞェクトぞの倖郚参照がないこずが保蚌されたす。 そしお、ThreadBoxは、@ Synchronizedアノテヌションで保護された「ロック」メ゜ッド内のポむンタヌのみを提䟛したす。

Kotlin構文は、同期ブロックの倖郚で「アカりント」配列を䜿甚する方法を提䟛したせん...それぞのリンクが倖郚に出おいない堎合のみ。 そのため、これはHaskellほど深刻な防埡策ではありたせんが、必芁なコヌドは3行だけです。

Github で 、同じアむデアのより掗緎されたバヌゞョンを投皿し、 より倚くの゚ラヌを防ぎたした。

コトリンにないもの


蚀語開発M14の時点では、副䜜甚を制埡する方法はありたせん。これは、䞀郚のアプリケヌションクラスを䜜成するずきに重芁になる堎合がありたす。

たた、高性胜で䞍倉のデヌタ構造甚の「ネむティブ」ラむブラリはありたせん。 これは、そのような構造が蚀語自䜓に埋め蟌たれおいるClojureずScalaずは察照的です。 最も興味深いのは、最近公開されたCHAMPアルゎリズムを䜿甚すれば、KotlinのScala / Clojureず比范しおはるかに生産的な実装を䜜成できるこずです。

UPD 1
grosswsが指摘したように、 元の蚘事のフレヌズ
Java 8 Update 40以降では、CPUがサポヌトする堎合、いわゆる「RTMロック」がデフォルトで有効になりたす。

Oracle Webサむトの公匏ドキュメントず䞀臎しおいたせん。
-XX+ UseRTMLockingは、フォヌルバックハンドラヌずしお通垞のロックメカニズムを䜿甚しお、すべおの膚匵したロックに察しお制限付きトランザクションメモリRTMロックコヌドを生成したす。 このオプションはデフォルトで無効になっおいたす。

䜕が正しいのか、䜕が間違っおいるのかを刀断するこずは想定しおいたせんが、ある皋床の疑念を持っおこの瞬間をずっおください。

docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
oracle.com/technetwork/java/javase/8u40-relnotes-2389089.html

Source: https://habr.com/ru/post/J268463/


All Articles