Elmで入門する関数型プログラミングの世界
関数型プログラミング初心者が純粋関数型言語であるElmに入門してみた備忘録です。
本記事の成果物はこちら
対象読者
- 関数型プログラミングを学びたいと思ってる方
- Elmを学びたいと思ってる方
モチベーション
筆者は普段サーバーサイドエンジニアとしてコードを書いていますが最近設計やテストについて学習する中で質の良い単体テストを書きたいという気持ちが強くなってきました。
質の良い単体テストを書くにはどうすればいいかを突き詰めた結果副作用のない関数設計をしたくなり、関数型プログラミングに入門してみることにしたというのがモチベーションです。
なのでゴールとしては単体テストが書きやすいような関数設計をするための何かしらを今回の入門で感じれればいいかなという感じです。
なぜElm?
筆者は以前「JavaScript関数型プログラミング」という技術書を読み、Kotlinで関数型プログラミングをするためのArrowというライブラリを使用し、関数型プログラミングに入門したことがあります。
しかし、実際に書いてて思ったのです。「書いてるコードがちゃんと関数型っぽく書けてるのかわからん!!」と。
なんか学んだことを実践してみただけで、よくわからなかったのが正直なところです。
そんなとき、「マルチパラダイムな言語で関数型をやると正解かわからないから純粋関数型言語で入門した方がいい」というアドバイスをもらいまして、さらにElmがおすすめということで機会があったらElmに入門しようと思ったのがElmに入門することにしたきっかけです。
以下の記事の作者様からアドバイスをいただきました!(とても納得感のある素晴らしい記事でした。)
Elmとは
Elmの公式ドキュメントの日本語訳では以下のように書かれています。
Elm は JavaScript にコンパイルできる関数型プログラミング言語です。 ウェブサイトやウェブアプリケーションを作るのに役立ちます。Elm はシンプルであること、簡単に使えること、高品質であることを大切にしています。
上記の通り、ElmはWebアプリケーションの画面を構築するための純粋関数型言語です。現代のフロントエンド開発ではReactやNextといったフレームワークを使用した開発が主流だと思いますが、ElmはThe Elm Architecture(TEA)という設計パターンで開発する言語です。
簡単にまとめるとElmは以下のような特徴があります。
- 純粋関数型言語
- TEAというシンプルで安全に画面構築ができるためのアーキテクチャが組み込まれている
- 言語仕様がシンプルで学習しやすい
Elmは新しい機能や言語仕様を追加することに非常に慎重なように感じました。これは筆者が普段書いているGo言語に似た思想を感じ、非常に好感が持てました。(言語仕様がシンプルで学びやすいことは良いこと。)
本記事ではElmの基本文法などの説明はしませんので、Elmを学びたい方はまずは以下の公式ドキュメントを一読することをおすすめします。すぐ読めるくらいの分量ですし、Elmコミュニティの方々が公式ドキュメントを日本語に翻訳してくれている大変ありがたいドキュメントです。
(本格的にElmのコードを読んでいるとおそらくlet-in
や|>
、<|
といったパイプ演算子、ラムダ式などが登場してきますが上記のサイトはElmに入門するための手引きをしてくれるガイドなので詳しくElmの文法を知りたい方は以下のサイトなどを参照してください。)
webアプリケーションを作る
さっそくですがElmでwebアプリケーションを作成していきたいと思います。Elmや関数型言語に入門したいという方はまずは上記の公式ドキュメントでElmの書き方やTEAの仕組みを学ぶことをおすすめします。可能であれば、入門書レベルでいいので何かしらの関数型プログラミングの技術書などを読んでおくと理解が進むと思います。最低限関数型プログラミングの登場人物(関数合成とか副作用とかモナドとかカリー化とか)が何かなんとなく知ってるとElmを書きながら答え合わせのようなことができると思います。
今回はいつも作っているガチャのシミュレーションアプリを作成したいと思います。
セットアップ
とりあえず、Elmのインストールします。
以下からインストーラー経由でインストールします。
% elm --version
0.19.1
# nodeも必要なのでインストールされてない方はインストール必要です
% node -v
v20.6.1
次にVSCodeの拡張機能を入れていきます。VSCode以外のエディタを使用している方はこちらを参照してください。
拡張機能はこちら
(elmで検索すると別の拡張機能がヒットするかもしれませんが非推奨になっているのでインストールするのはElm toolingから出ている方。)
フォーマット用に以下もインストール
% npm install -g elm-format
プロジェクトの作成にはいろいろ方法があるみたいで以下の記事が参考になりました。
ざっくり以下のような方法がたぶんあります。
- elm reactor elmに組み込まれてるのですぐ使える。
- create-elm-app create-react-appのElm版。動かしてみたけど起動時にエラー吐いたのでやめた
- vite 公式のテンプレートはないのでコミュニティ提供のテンプレートを使うか、自力で構築
- elm-spa ElmでSPAやるなら。
- elm-pages ElmでSSG的なことやりたいとき。
elm-spa
やelm-pages
は今回の入門にはやりすぎ感があったのと、一応見た目を整えたく慣れてるtailwindを採用することも考えて今回はviteを採用しました。
viteとTailwindを使うには以下の記事を参考にさせていただきました。
% bunx create-vite --template vanilla-ts demo-app
% elm init
% bun i -d vite-plugin-elm
特に意味はないですがbunを使用してcreate-vite
を実行します。テンプレートはvanilla-ts
を使用します。次にelmの方の初期化でelm init
を実行します。これでelm.json
が作成されると思います。Elmで書いたコードをビルドに組み込むためにvite-plugin-elmをインストールします。インストールが完了したら以下のconfigファイルを作成して完了です。
import { defineConfig } from "vite";
import elmPlugin from "vite-plugin-elm";
export default defineConfig({
plugins: [elmPlugin()],
});
次にmain.ts
を以下のように修正します。
import { Elm } from "./Main.elm";
Elm.Main.init({ node: document.querySelector("#app") });
これでElmで書いたプログラムを組み込むことができましたが、Elmモジュールの型定義がなくてエラーが出ていると思うので以下のように型定義ファイルも作成します。
export var Elm: any;
(型付は今回anyにしてしまっていますがより厳密に型定義することも可能なようです。こちらを参照)
次にMain.elm
ファイルを作成してとりあえず公式ドキュメント記載のカウンタープログラムをコピペして動かしてみます。
Main.elm
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
main =
Browser.sandbox { init = 0, update = update, view = view }
type Msg = Increment | Decrement
update msg model =
case msg of
Increment ->
model + 1
Decrement ->
model - 1
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model) ]
, button [ onClick Increment ] [ text "+" ]
]
% bun dev
tailwindの導入
tailwindを以下のようにインストールします。
% bun i -d tailwindcss@latest postcss@latest autoprefixer@latest
設定ファイルを作成
% bunx tailwindcss init -p
作成できたらtailwind.config.js
を以下のように修正
module.exports = {
content: ["index.html", "./**/*.{css,ts,elm}"],
theme: {
extend: {},
},
plugins: [],
}
最後にmain.tsにtailwindをimportするように修正して完了です。
import { Elm } from "./Main.elm";
++ import "tailwindcss/tailwind.css";
Elm.Main.init({ node: document.querySelector("#app") });
あとはVSCodeを使用している方はElmを書いている時にtailwindの補完を効かせたいと思いますのでこちらを参照して設定することをおすすめします。
Elmのプログラムを書く
いよいよ本題のElmのプログラムを作成していきます。まずは以下のようにカスタム型と型エイリアスを定義しました。
型定義
type Rarity
= N
| R
| SR
type alias Item =
{ rarity : Rarity
, name : String
, weight : Int
}
-- Model
type alias Model =
{ result : Maybe Item
}
-- Msg
type Msg
= Draw
| GenerateRandomValue Int
Item
型がガチャから排出されるアイテムを表しています。今回はレア度とアイテム名だけ設定しています。weight
というフィールドはガチャの抽選ロジックで重み付け抽選をするときに使用する想定で設定しています。
次にModelですがElmにおけるModel型はアプリケーションの状態を表します。今回はガチャで抽選したアイテムの情報だけModelで管理するものとします。
Msgの型定義は画面更新するトリガーのようなものです。今回はDraw
とGenerateRandomValue
という二つを定義しました。Draw
はガチャを引く時に発行される想定です。GenerateRandomValue
は後述する乱数生成のためのMsgとなります。
ガチャの抽選ロジックを実装する
ここでガチャの抽選の流れを確認したいと思います。流れとしてはざっくり以下のような流れです。
- ガチャに含まれる全てのアイテムの重みの総和を求める。
- 重みの総和の範囲で疑似乱数を生成する。
- アイテムの重みを足していって乱数の重みを超えたらそのアイテムを排出する。
手続き型のプログラミングで書くならこれらの処理を順番に実行する関数を定義するだけです。ただし、今回は純粋関数型言語であるElmで書くので手続き型とは書き方が異なります。特に乱数の生成の仕方についてはなるほどーとなりました。
まず1の重みの総和を求める関数を以下のように定義しました。
-- トータルの重みを算出
getTotalWeight : List Item -> Int
getTotalWeight items =
foldl (\item acc -> acc + item.weight) 0 items
List Item
を引数にとり重みの総和をInt
で返す関数を定義しました。関数型っぽいところは関数型プログラミングではforやwhileといったループ構文を使わずにmapやreduceといった高階関数を使用することが一般的です。今回はfoldl
という関数を使用してアイテムの総和を求めるような関数を作成しました。
次に2の疑似乱数の生成ですがこれにはElmのelm/random
モジュールを使用します。モジュールのインストールは以下を実行します。
% elm install elm/random
作成した関数は以下のようになります。
-- 乱数生成
generateRandomValue : Int -> Cmd Msg
generateRandomValue total =
Random.generate GenerateRandomValue (Random.int 0 total)
この関数は重みの総和をInt
型で引数にとりCmd Msg
を返す関数です。ElmではCmd Msg
を返す関数を実行することでTEAにおけるupdate
関数の実行がされます。つまり、この関数は疑似乱数を生成して返すのではなく疑似乱数を生成するメッセージを発行する関数です。
疑似乱数の生成は代表的な副作用となりますがElmではこのようにCmd Msg
を経由することで疑似乱数の生成という副作用が関数内に含まれないようになっているようです。すごい!!
最後に乱数を使用してアイテムを抽選する関数を以下のように定義しました。
-- アイテム抽選
lottery : List Item -> Int -> Maybe Item
lottery items randomValue =
lotteryHelper items randomValue 0
lotteryHelper : List Item -> Int -> Int -> Maybe Item
lotteryHelper items randomValue acc =
case items of
[] ->
Nothing
item :: rest ->
let
newAcc =
acc + item.weight
in
if newAcc >= randomValue then
Just item
else
lotteryHelper rest randomValue newAcc
ガチャに含まれるアイテム一覧と生成した疑似乱数を引数に取り、抽選したアイテムを返す関数を作成しました。返すアイテムは抽選失敗することを考慮してMaybe Item
型としました。
これも関数型っぽいところだと思いますが関数型プログラミングでは他の言語にあるようなnullやnilといった値が存在しません。関数が値を返さない場合があればMaybe
型を使用することで値をラップします。これは関数に参照透過性を持たせ副作用のない純粋関数を作るための重要な概念です。(という理解ですがもし間違ったことを言ってたらコメントください。。)
さらにlottery
関数はlotteryHelper
関数を呼び出していますがloteryHelper
関数は再起処理をする再帰関数となっています。これも関数型の特徴のひとつだと思われますが前述したように関数型プログラミングではforやwhileといったループ処理の代わりにこういった再帰関数を利用します。
再帰関数内では乱数の重みを越えるまでアイテムの重みを再帰的に加算し、抽選に成功した場合はアイテムを返します。もし、抽選ができなかった場合はMaybe.Nothing
を返すような関数になっています。
この関数の呼び出し元ではこの関数の戻り値であるMaybe
型をパターンマッチングでハンドリングすることになります。
(この関数で抽選アイテムが見つからないのは明らかなシステムバグなのでResult
型の方がもしかしたらいいのかもしれないと書いていて思いましたがとりあえずMaybe
型にしておきます。)
実装した抽選ロジックを呼び出す
次に作成した関数の呼び出し部分を作っていきます。Elmでは画面描画処理をview
関数として以下のように作成します。
view : Model -> Html Msg
view model =
div [ class "w-screen h-screen flex justify-center items-center flex-col bg-slate-800" ]
[ img [ src "/public/gachagacha.png", class "h-70 w-50" ] []
, button [ onClick Draw, class "mt-10 p-5 rounded bg-indigo-500 hover:bg-indigo-300" ] [ text "ガチャを引く" ]
, div [ class "mt-10 grid grid-cols-1" ]
[ div [ class "font-bold text-lg text-pink-400 col-span-1 text-center mb-2" ] [ text "Result" ]
, div [ class "col-span-1" ]
[ div [ class "grid grid-cols-6 text-center text-white" ]
[ div [ class "col-span-3 -white" ] [ text "rarity" ]
, div [ class "col-span-3 text-center -white" ] [ text (getDisplayRarity model) ]
]
]
, div [ class "col-span-1" ]
[ div [ class "grid grid-cols-6 text-center text-white" ]
[ div [ class "col-span-3" ] [ text "item name" ]
, div [ class "col-span-3 text-center" ] [ text (getItemName model) ]
]
]
]
]
詳細な説明は省略しますがclass属性に指定しているのはtailwindのクラスです。大事な部分は以下のDraw
メッセージをクリックイベントで発行するbuttonタグです。
button [ onClick Draw, class "mt-10 p-5 rounded bg-indigo-500 hover:bg-indigo-300" ] [ text "ガチャを引く" ]
このボタンがクリックされるとDraw
メッセージが発行され以下のupdate
関数の処理に渡ります。
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Draw ->
( model, getTotalWeight gacha |> generateRandomValue )
GenerateRandomValue randomValue ->
( { model | result = lottery gacha randomValue }, Cmd.none )
update
関数は引数にModel
とMsg
型を取り、Msg型をパターンマッチングすることで特定の処理を実行します。
今回はガチャを引くボタンがクリックされることでDraw
メッセージが渡ってきているので以下の処理が実行されます。
( model, getTotalWeight gacha |> generateRandomValue )
定義されたガチャのアイテム一覧を引数に取り、getTotalWeight
関数で重みの総和を求めて、乱数を生成するgenerateRandomValue
に引数として渡ります。
generateRandomValue
関数は乱数を返すのではなくCmd Msg
型を返すので新たにメッセージを発行します。そのため、乱数を外部で生成したのち再度update
関数の以下の処理に入ります。
( { model | result = lottery gacha randomValue }, Cmd.none )
この処理でガチャに含まれるアイテム一覧と生成した乱数を引数にガチャの抽選が行われ、その結果をmodelに反映して処理が終わります。
これで無事アイテムが抽選されていれば更新されたアイテム情報が画面に描画されるような流れです。
最終的なElmのコードは以下のようになりました。
最終的なMain.elm
module Main exposing (..)
import Browser
import Html exposing (Html, button, div, img, text)
import Html.Attributes exposing (class, src)
import Html.Events exposing (onClick)
import List exposing (foldl)
import Maybe exposing (map, withDefault)
import Platform.Cmd as Cmd
import Random
type Rarity
= N
| R
| SR
type alias Item =
{ rarity : Rarity
, name : String
, weight : Int
}
-- Model
type alias Model =
{ result : Maybe Item
}
-- Msg
type Msg
= Draw
| GenerateRandomValue Int
-- ガチャに含まれるアイテム一覧
gacha : List Item
gacha =
[ { rarity = N, name = "item1", weight = 10 }
, { rarity = N, name = "item2", weight = 10 }
, { rarity = R, name = "item3", weight = 5 }
, { rarity = R, name = "item4", weight = 5 }
, { rarity = SR, name = "item5", weight = 2 }
]
getRarity : Item -> String
getRarity item =
case item.rarity of
N ->
"N"
R ->
"R"
SR ->
"SR"
getDisplayRarity : Model -> String
getDisplayRarity model =
model.result
|> map getRarity
|> withDefault ""
getItemName : Model -> String
getItemName model =
map (\item -> item.name) model.result
|> withDefault ""
-- トータルの重みを算出
getTotalWeight : List Item -> Int
getTotalWeight items =
foldl (\item acc -> acc + item.weight) 0 items
-- 乱数生成
generateRandomValue : Int -> Cmd Msg
generateRandomValue total =
Random.generate GenerateRandomValue (Random.int 0 total)
-- アイテム抽選
lottery : List Item -> Int -> Maybe Item
lottery items randomValue =
lotteryHelper items randomValue 0
lotteryHelper : List Item -> Int -> Int -> Maybe Item
lotteryHelper items randomValue acc =
case items of
[] ->
Nothing
item :: rest ->
let
newAcc =
acc + item.weight
in
if newAcc >= randomValue then
Just item
else
lotteryHelper rest randomValue newAcc
-- init
init : () -> ( Model, Cmd Msg )
init _ =
( { result = Nothing
}
, Cmd.none
)
-- update
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Draw ->
( model, getTotalWeight gacha |> generateRandomValue )
GenerateRandomValue randomValue ->
( { model | result = lottery gacha randomValue }, Cmd.none )
-- subscriptions
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
-- view
view : Model -> Html Msg
view model =
div [ class "w-screen h-screen flex justify-center items-center flex-col bg-slate-800" ]
[ img [ src "/public/gachagacha.png", class "h-70 w-50" ] []
, button [ onClick Draw, class "mt-10 p-5 rounded bg-indigo-500 hover:bg-indigo-300" ] [ text "ガチャを引く" ]
, div [ class "mt-10 grid grid-cols-1" ]
[ div [ class "font-bold text-lg text-pink-400 col-span-1 text-center mb-2" ] [ text "Result" ]
, div [ class "col-span-1" ]
[ div [ class "grid grid-cols-6 text-center text-white" ]
[ div [ class "col-span-3 -white" ] [ text "rarity" ]
, div [ class "col-span-3 text-center -white" ] [ text (getDisplayRarity model) ]
]
]
, div [ class "col-span-1" ]
[ div [ class "grid grid-cols-6 text-center text-white" ]
[ div [ class "col-span-3" ] [ text "item name" ]
, div [ class "col-span-3 text-center" ] [ text (getItemName model) ]
]
]
]
]
-- main
main : Program () Model Msg
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
テストを書く
アプリケーションの実装は以上になりますが最後に作成した関数のテストコードを書いて終わりとします。ElmのテストをCLIで実行するには一般的にnode-test-runnerをインストールして使うようです。
ただ、今回はRustで作られたテストランナーでelm-test-rsというものがあるみたいだったのでこちらを使用してみました。
グローバルインストールしてもいいですが、今回はプロジェクトにローカルインストールしました。
% bun i -d elm-test-rs
% bunx elm-test-rs init
The file tests/Tests.elm was created
% bunx elm-test-rs
Running 1 tests. To reproduce these results later,
run elm-test-rs with --seed 597517184 and --fuzz 100
◦ TODO: Implement the first test. See https://package.elm-lang.org/packages/elm-explorations/test/latest for how to do this!
TEST RUN INCOMPLETE because there is 1 TODO remaining
Duration: 1 ms
Passed: 0
Failed: 0
Todo: 1
作成されたTests.elm
にテストを書いていきます。
module Tests exposing (..)
import Expect exposing (equal, notEqual)
import Fuzz exposing (intRange)
import Main exposing (Rarity(..), gacha, getRarity, lottery)
import Test exposing (Test, describe, fuzz, test)
suite : Test
suite =
describe "The Main module"
[ test "Hello" <|
\_ ->
let
act =
"Hello"
in
equal act "Hello"
, fuzz (intRange 0 32) "lottery fuzzy test" <|
\weight ->
notEqual (lottery gacha weight) Maybe.Nothing
]
getRarityTest : Test
getRarityTest =
describe "getRarityTest"
(List.map
(\( item, expected ) ->
test ("Testing getRarity with " ++ item.name) <|
\_ -> getRarity item |> Expect.equal expected
)
[ ( { rarity = N, name = "Normal Item", weight = 10 }, "N" )
, ( { rarity = R, name = "Rea Item", weight = 10 }, "R" )
, ( { rarity = SR, name = "SuperRea Item", weight = 10 }, "SR" )
]
)
今回作成したアプリケーションの最も重要な抽選関数のテスト部分は以下の部分です。
fuzz (intRange 0 32) "lottery fuzzy test" <|
\weight ->
notEqual (lottery gacha weight) Maybe.Nothing
詳細な説明は省略しますがこれは関数型言語でよく書かれるFuzzテストなどと呼ばれるものです。これはさまざまなエッジケースを含む多くの入力パターンをテストするテスト手法です。
このテストでは0-32
の範囲で重みの総和を関数に渡しアイテムが抽選できていることをテストしています。0-32
という範囲は実際のアプリケーションで使用している以下のアイテム一覧の重みの範囲です。
gacha : List Item
gacha =
[ { rarity = N, name = "item1", weight = 10 }
, { rarity = N, name = "item2", weight = 10 }
, { rarity = R, name = "item3", weight = 5 }
, { rarity = R, name = "item4", weight = 5 }
, { rarity = SR, name = "item5", weight = 2 }
]
もう一つのテストはgetRarity
というレアリティを文字列で返す関数のテストです。これは全てのレアリティに対してテストを網羅したかったのでデータ駆動テストやテーブル駆動テストとよばれるテスト手法で書いています。
getRarityTest : Test
getRarityTest =
describe "getRarityTest"
(List.map
(\( item, expected ) ->
test ("Testing getRarity with " ++ item.name) <|
\_ -> getRarity item |> Expect.equal expected
)
[ ( { rarity = N, name = "Normal Item", weight = 10 }, "N" )
, ( { rarity = R, name = "Rea Item", weight = 10 }, "R" )
, ( { rarity = SR, name = "SuperRea Item", weight = 10 }, "SR" )
]
)
% bunx elm-test-rs -v
elm-test-rs 3.0.0 for elm 0.19.1
--------------------------------
✓ Compilation of tests modules succeeded
Running 5 tests. To reproduce these results later,
run elm-test-rs with --seed 4221462656 and --fuzz 100
Tests listing:
↓ The Main module
✓ PASSED: Hello
✓ PASSED: lottery fuzzy test
↓ getRarityTest
✓ PASSED: Testing getRarity with Normal Item
✓ PASSED: Testing getRarity with Rea Item
✓ PASSED: Testing getRarity with SuperRea Item
TEST RUN PASSED
Duration: 3 ms
Passed: 5
Failed: 0
Running duration (since Node.js start): 55 ms
テストが無事全てパスしました!
感想
以上、駆け足でしたがElmを通して関数型プログラミングに入門してみました。以下やってみた感想です。
関数型プログラムに目を慣らす
ElmはTEAに従ってModelやview, update関数を定義すればいいようになっているのでそこまで困惑することはなかったですが手続き型のプログラムとぱっと見の印象が全然違うように感じました。最初let-in
の構文を知らないで書いていた時は一呼吸で処理を実行しないといけないと思いコンパイル通すのも一苦労してしまいました。
感覚的な話ですが手続き型のプログラムだとif文やfor文が多く目につきますが今回作成したプログラムをみるとパイプ演算子
で処理をつないだり、mapやcase文などが多く目につきます。
確かにこういった関数型の書き方が強制されるのでElmのような純粋関数型言語で関数型に入門するのは非常に有効だなと感じました。
Elmの書き心地
Elmは関数型言語なので関数型言語に慣れていないと難しく感じると思いますが、それでもElmの言語仕様はそれほど多くないのでそこまで苦労せず書くことができました。加えて、TEAというプログラムを書くための道標のようなものがあるためより書きやすく感じました。
最初は慣れない書き方に戸惑いましたが慣れると非常に楽しかったです。
MaybeやResult型
既存言語でも採用していたりするようですがMaybe
型とResult
型があることで副作用の少ない関数につながるんだろうなと思い、みなさんがこれらの型を欲しがる理由がなんとなくわかりました。
乱数の生成について
前述してますが疑似乱数の生成は副作用です。手続き型の言語だと意識して書かないと関数内で乱数を生成してしまい、テストが書けないということがよくあります。これが今回Cmd Msg
を経由することで副作用の出ないように関数を自然と作成することができました。感動
再帰関数
lottery
という関数内で再起処理をするのに今回lotteryHelper
という関数を定義しましたが正直命名が微妙だなと思っていてこういう時どういう命名するのかどなたか関数型詳しい方教えてください。
関数型プログラミングの学習について
Elmは純粋関数型言語なのでElmを書くことで関数型の書き方が自然とできてるんだと思うんですが、これは関数型の概念でこういうものですみたいな説明は特にElmの公式ガイドなどにはないのでざっくり関数型の知識がある方が理解度は上がるような気がしました。今回特にやってないですが関数のカリー化がいまいち何でやるのかわからなかったのですがElmを知ったことでなんとなくですが理解できた気がします。
テストについて
実際テストは書きやすかったと思うんですがFuzzテストはあれでよかったのかよくわかってないです。入力のパターンを無数に生成できるのは理解したのですが、それに対して期待値を設定してテストをしなければならないと思うんですが、期待値の設定の仕方がいまいちわかってないです。結局無数の入力パターンに対して成功するような期待値を設定しなければならないと思うのですが、その方法がいまいちピンときてません。これは課題としてもう少し学習していきたいなと思います。
あとは、関数の数が手続き型と比べて多くなってくるのでどの関数をテストして、どの関数はテストしないのかみたいな判断が正直あんまりピンときてないです。
まとめ
まだまだ学ぶことが多いですがElmを通して関数型プログラミングがどういうものかが前より理解できた気がします。そして、今回ゴールとして設定していた単体テストをうまく書くための関数の書き方については正直理解したとは言えないですがElmの関数がお手本だと思えば良い経験になったと思います。加えて、Fuzzテストのような関数型の世界で使用されているテスト手法などもより知ることができました。
本記事が関数型を学ぼうとしている方の参考に少しでもなれば幸いです。
今回は以上です🐼
Discussion
まずはじめにElm入門したばかりなのに、とてもチャレンジングで素晴らしい完成度の記事と感じました!
なのでとても細かいところで気になったものです⇩
文中で、
getTotalWeight
という関数が出てきましたがList.sumを使えば処理の間違える可能性を減らせそうです。今回で言えば、foldで前の結果に今見ている値を足し込む, 0から始める などの処理における注意が必要です。例えば、掛け算の場合は0から始めてしまうとどんなに掛けても0になってしまうようにですね。
こう言ったよく書かれる処理は一般化し(今回だとList.sum関数)それらを組み合わせるだけのコードを書くことが、関数型プログラミング言語らしさのひとつと思います。そうすると、もはやテストすら必要がない、関数に切り出さずインラインに書ける部分が増えコードが描きやすくなるという利点が生まれます(もちろん複雑になったらテストは書くべきですが)
もうひとつ気になったのはこちらです。
副作用が絡んでくる部分は、あくまで変数のような再代入を許すような言語機構の場合は、nil, null等に気をつけましょうという話な気がします。
Maybeは、ぱんださんが定義した
type Rarity
と同じように、値がある時・ない時を制限している(一般化された)型に過ぎません。直接の恩恵は、副作用のラップではなく、パターンマッチ時に値がある時・ない時、両方網羅して分岐を書かなければならない強制力にあります。また、一般化していることによる豊富な関数群があることでしょうか。
もちろん、もっと踏み込めばいろんな話ができる型ではありますが、軸は網羅性の部分かなと思います。
以上、長々とすみませんでした。
ありがたいお言葉ありがとうございます🙇♂️
getTotalWeight
について。なるほど、そのように書けばいいんですね!テストすら必要がないっていう感覚は手続き型でやっているとあんまり思わなそうな感覚ですね。例にしている関数の処理がそんなに複雑ではないというのがありますが。勉強なります!
Mabe
について。なるほどです!Maybeの恩恵はパターンマッチ時の強制力というのは非常に納得できました!ありがとうございます!