HN 日本語サマリー

← 一覧へ戻る
プログラミング

hicaにおける関数型プログラミング

Functional Programming in hica (hica.dev)

38 pointsby cladamski799 コメント

要約

hicaは、状態を変更する命令のシーケンスを記述するのではなく、関数を合成してプログラムを構築する関数型プログラミング(FP)スタイルを中心に設計されています。hicaでは、データはデフォルトで不変(immutable)であり、ほとんどすべてが式(expression)として扱われ、関数は第一級の値として扱われます。これにより、コードの推論やテストが容易になり、より堅牢なプログラム開発が可能になります。

全文翻訳

hicaにおける関数型プログラミング関数型プログラミング(FP)は、状態を変更する命令のシーケンスを記述するのではなく、関数を合成してプログラムを構築するスタイルです。hicaはこのスタイルを中心に設計されています:不変データ、あらゆる場所での式、そして第一級の値としての関数。すべての例は実行可能です。FPの背景は必要ありません。関数を書き、match式を読めれば十分です。ステートメントではなく式ほとんどの言語では、ステートメントは何かを行い、式は値を生成します。hicaでは、if、match、ブロックを含むほとんどすべてが式です。つまり、値が期待される場所ならどこでもそれらを使用できます:fun sign(x) => if x < 0 { "negative" } else { "non-negative" } {}ブロックの本体はその最後の式の値です。returnキーワードはありません:fun clamp(x, lo, hi) { if x < lo { lo } else if x > hi { hi } else { x } }この単一のルールは、FPのほとんどをカバーします:すべてに値がある場合、すべてを合成できます。デフォルトで不変FPは共有される可変状態を回避します。値を作成した後に変更できない場合、関数は推論やテストが容易になります。hicaでは、letは不変のバインディングに使用されます:let name = "Alicia" let scores = [85, 92, 78] scoresをインプレースで変更することはありません。代わりに、新しいリストを作成します:let updated = scores + [95] // 新しいリスト: [85, 92, 78, 95] let doubled = map(scores, (x) => x * 2) ミューテーション(カウンター、ループ変数)が必要な場合は、varを使用します。これはローカルスコープであり、関数から漏れ出すことはありません:var total = 0 for x in scores { total = total + x } varはオプトインです。それ以外のすべては不変のままです。純粋関数純粋関数は、同じ入力に対して常に同じ出力を返し、副作用(印刷、ファイルI/O、可変状態など)を持ちません。純粋関数はテストや合成が容易です。fun add(a, b) => a + b fun square(x) => x * x fun to_celsius(f: float) => (f - 32.0) * 5.0 / 9.0hicaでは、関数はデフォルトで純粋です。型システム(Kokaから継承)はI/Oのような効果を追跡するため、関数に副作用がある場合、その型にそれが表示されます。純粋関数は、あなたが構築するものです。それ以外のすべては合成です。第一級の値としての関数FPでは、関数は整数や文字列のような値です。変数に格納したり、他の関数に渡したり、関数から返したりできます。fun apply(f, x: int) => f(x) fun main() { let double = (x) => x * 2 let greet = (name) => "Hello, " + name println(apply(double, 5)) // 10 println(greet("Olle")) // Hello, Olle } 他の関数を受け取ったり返したりする関数は、高階関数と呼ばれます。上記のapplyはその一つです。クロージャクロージャは、周囲のスコープから変数をキャプチャする関数です:fun make_adder(n) => (x) => x + n fun main() { let add5 = make_adder(5) let add10 = make_adder(10) println(add5(3)) // 8 println(add10(3)) // 13 } make_adderは毎回新しい関数を返します。その関数はnをクロージャとしてキャプチャし、make_adderが返された後も、作成されたときのnの値を記憶しています。このパターンにより、必要に応じて特殊化された関数を大量に生成できます:fun make_multiplier(factor) => (x) => x * factor fun main() { let triple = make_multiplier(3) let nums = [1..5] println(map(nums, triple)) // [3, 6, 9, 12, 15] } map、filter、foldこの3つの関数は、リストを扱うために必要なほとんどすべてをカバーします。map: 各要素を変換するfun main() { let nums = [1..5] println(map(nums, (x) => x * x)) // [1, 4, 9, 16, 25] println(map(nums, show)) // ["1", "2", "3", "4", "5"] } mapはリストと関数を受け取ります。リストの各要素に関数を適用し、同じ長さの新しいリストを返します。filter: 一致する要素を保持するfun main() { let nums = [1..8] let evens = filter(nums, (x) => x % 2 == 0) println(evens) // [2, 4, 6, 8] } filterは、述語がtrueを返す要素のみを保持します。fold: 単一の値に削減するfun main() { let nums = [1..5] let total = fold(nums, 0, (acc, x) => acc + x) println(total) // 15 let product = fold(nums, 1, (acc, x) => acc * x) println(product) // 120 } foldは結果を累積します。初期値から始まり、各要素に順番に関数を適用します:accは実行中の結果を保持し、xは現在の要素です。foldを使用して多くのリスト操作を実装できます:fun my_length(xs) => fold(xs, 0, (acc, _) => acc + 1) fun my_max(xs) => fold(xs, 0, (acc, x) => if x > acc { x } else { acc }) fun my_reverse(xs) => fold(xs, [], (acc, x) => [x] + acc) flattenとflat_mapmapは各要素を変換します。しかし、mapに渡す関数自体がリストを返すことがあります。結果はリストのリストになり、通常は望ましくないものです:fun main() { let sentences = ["hello world", "foo bar", "one two three"] let split_words = map(sentences, (s) => split(s, " ")) println(split_words) // [["hello", "world"], ["foo", "bar"], ["one", "two", "three"]] } すべての単語のフラットなリストが必要でした。2つの関数がこれを解決します。concat: ネストを1レベル折りたたむfun main() { let nested = [[1, 2], [3, 4], [5, 6]] println(concat(nested)) // [1, 2, 3, 4, 5, 6] } concatはリストのリストを受け取り、1レベル折りたたみます。(他の言語ではflattenと呼ばれます。)flat_map: mapとflattenを1ステップでfun main() { let sentences = ["hello world", "foo bar", "one two three"] let all_words = flat_map(sentences, (s) => split(s, " ")) println(all_words) // ["hello", "world", "foo", "bar", "one", "two", "three"]}flat_mapは、各要素に関数を適用し、結果のすべてのリストを結合します。これはmapの後にconcatを実行するようなものですが、1ステップで記述されます:flat_mapは、mapされた関数がリストを返す場合に常に使用します。パイプでの要素の展開flat_mapはパイプに自然に適合します。単一の要素が複数の要素になるステップで使用します:fun expand(n) => [n, n * 10] // 各要素が2つに展開 fun main() { let result = [1, 2, 3] |> flat_map(expand) // [1, 10, 2, 20, 3, 30] |> filter((x) => x > 5) // [10, 20, 30] println(result) } flat_mapがない場合、[[1, 10], [2, 20], [3, 30]]となり、filterは数値ではなくリストに対して操作されます。同じ考え方がMaybeにも適用されますパターン:ラップされた値を生成する関数を適用し、その後ラッパーをフラット化する、という操作はMaybeでも現れます。map_maybeはMaybe内の値を変換します。しかし、関数自体がMaybeを返す場合、Maybe<Maybe<x>>になってしまいます。and_thenは、余分なレイヤーを自動的にフラット化することで、それを防ぎます:fun parse_pos(s: string) : maybe<int> { let n = parse_int(s)? if n > 0 { Some(n) } else { None } } fun main() { // and_thenはそれぞれMaybeを返すステップを連鎖させる — ネストなし、ネストされたmatchなし let result = Some("42") |> and_then((s) => parse_int(s)) // 文字列をパース → maybe<int> |> and_then((n) => parse_pos(show(n))) |> map_maybe((n) => n * 2) println(result) // Some(84) let bad = Some("-5") |> and_then((s) => parse_int(s)) |> and_then((n) => parse_pos(show(n))) |> map_maybe((n) => n * 2) println(bad) // None } リストのflat_mapとMaybeのand_thenは、名前は異なりますが同じ考え方です。関数型プログラマーはこの操作をbindと呼びます。パターンを知ること:「ラップされた値に対して、それ自体がラップされた値を返す関数でマップし、二重にラップしない」ということは、型に関わらず、覚えておくべきことです。|>による合成パイプ演算子|>は、ある式の結果を次の関数に渡します。左から右に読み取られ、操作の順序と一致します:fun main() { let result = [1..10] |> filter((x) => x % 2 == 0) // [2, 4, 6, 8, 10] |> map((x) => x * x) // [4, 16, 36, 64, 100] |> fold(0, (acc, x) => acc + x) // 220 println(result) } |>がない場合、次のように記述します:let result = fold(map(filter([1..10], (x) => x % 2 == 0), (x) => x * x), 0, (acc, x) => acc + x) |>を使用すると、各ステップは独自の行に配置され、変換の順序で読み取ることができます。それがポイントです:各ステップは1つの関数であり、それらがチェーンされます。ポイントフリースタイルラムダが引数を直接関数に渡すだけの場合、ラムダ全体を省略できます:fun is_even(x) => x % 2 == 0 fun square(x) => x * x fun main() { let