Open193

Haskell の学習ログ (基礎~Stateモナドぐらいまで)

楽しそうなのでHaskellを学ぶだけ.

(人間の)スペック

  • TypeScriptまあまあわかる
  • ADTが何かまあまあわかる
  • Rustちょっとだけやった
    • CopyとCloneしとけばなんとかなるっしょ?
  • Elmちょっとだけやった
  • 高1でやる程度のかんたんな集合とブール論理はわかる
  • 論理学者 ハスケル・カリー (Haskell Curry) の名前に由来している.
    • https://en.wiktionary.org/wiki/Haskell によれば
      1. 古ノルド語の名 (下の名前) Áskellが父称になって生まれた姓
      2. エゼキエル (旧約聖書に登場するユダヤ人の預言者) に由来するユダヤ系の姓
      3. これらの姓から転用された名
    • 父の名前サミュエルもどうやらユダヤ系っぽいのでたぶん家はユダヤ系なのだろう.勘違いかも
  • 純粋関数型
  • 遅延評価
  • 主要な処理系はGHC (Glasgow Haskell Compiler)
    • 名前の通りコンパイラ
  • 遅延評価って言語の特徴ってことでいいんだろうか
    • 処理系の特徴とかでは?
    • 言語仕様に「処理系は遅延評価で作れよ」って書いてあったりするのかもしれない
  • 関数型と手続き型とを対比させようとして出される関数型の例って宣言型なのでは?
    • 宣言型がよくわからないので適当言ってる
    • 宣言的というのがよくわかってない
    • 定義の集合って感じ?

環境構築はなんとなくghcupを使う.選定理由はrustupと名前が似てるから.

https://www.haskell.org/ghcup/
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh

プラットフォームごとに (おそらくユーザーエージェント文字列を見て) 案内を出し分けているようだが,Windows向けの案内にしっかりWSLへの案内も載っててナイス.

書き忘れてたけど Windows 10 Pro 20H2 の Ubuntu 20.04 on WSL2.

依存パッケ

sudo apt install build-essential curl libffi-dev libffi7 libgmp-dev libgmp10 libncurses-dev libncurses5 libtinfo5

適当にEnterポチポチすると勝手に進む.最後にHaskell Language Serverを入れるか訊かれる.VSCodeで開発するときに使うので今入れちゃう (VSCodeでソース開いた時にも訊かれるようなのでそのタイミングで入れてもよい).

  • Cabal vs Stack という対立構造がある
  • npm vs Yarn っぽい
    • Stack / Yarn は依存関係地獄の救世主として現れた
    • Cabal / npm も負けじと機能改善を行ってどっちでもいい感じになった

VSCodeのHaskell拡張を入れる.インストール数が妙に少ないのは最近新しく出たからっぽい.

https://marketplace.visualstudio.com/items?itemName=haskell.haskell
  • Language Server
    • Language Server Protocol に沿ってエディタの注釈とかを提供する
    • Haskell Language Server が推奨
    • Haskell IDE Engine は legacy で probably shouldn't use this one で replaced by HLS らしい
    • HLSはghcideの上に構築されてるらしいが表面に出ないのでどうでもいい

Cabalのプロジェクト構築する npm init -y 的なの

cabal init

ディレクトリ名.cabal って名前でYAMLっぽいけどなんか違う設定ファイルが生えた.依存関係とかライセンスとか書くようなので package.json みたいな感じそう

cabal init が自動的に Hello, world を生やし,手書きする愉しみを理不尽にも奪われる.

Main.hs
module Main where

main :: IO ()
main = putStrLn "Hello, Haskell!"

cabal run で実行される.cargo run じゃん.影響の順序は逆か?

別にCabalなんざ使わずとも ghc さえ入ってれば ghc Main.hs で実行ファイル作れるし runghc Main.hs で直接実行できる.とほほの入門ではこっち.入門の段階でパッケージ管理とか大仰なものが出てくると圧倒されるので,素朴な方法で体験できるのは重要だと思う.

Hello worldからも読み取れることがある.

  • エントリポイントは main
  • mainIO () を返す
    • これは関数なのか?
  • 関数呼び出しには () が不要

putStrLn が標準出力してるんではなく,putStrLn が出力を表す IO 値を返してそれが main の値になって外側で出力が行われるらしい.

-- 行コメント
{-
  複数行コメント
-}

VSCodeでは Control-/ でその行をコメントアウト/コメント解除をトグルできる便利機能がある.-- を挿入するのに / を使わされるのは手続き型・オブジェクト指向というかC++/Java派生言語に囚われた人間の利己的な設計であり関数型への迫害である,的な言いがかりをつけられないでもない.

do で複数の式をまとめて実行できる.do {} としてブロックを書くこともできるらしいが,フォーマッタが優秀すぎて保存時に勝手に消されてインデントになる.

main = do
  putStr "Hello, "
  putStrLn "world!"

そういやElmのフォーマッタもめっちゃ改行とか突っ込んできたな…

do はモナドがどうたらみたいな話をどっかで見たことがあるのでとほほの「複数の式を実行できる」は言葉足らずなのでは?とひそかに疑っている.というか IO 返す関数が2個あるのに main の型が IO () のままなのがおかしい.Rustだとブロックの末尾の式がブロック全体の値になるわけで,だとしたら "Hello, " が出力されるのはなんか不自然.do が魔法によって単一の IO を返してたりするんだろう.

標準出力から1行取って出力するだけ.

main = do
  s <- getLine
  putStrLn s

s <- getLinegetLine (\s -> ...) のシンタックスシュガーで,要するに変数っぽいのは無名関数の引数名だ,みたいな話を読んだことがある.

JavaScriptで、ある式の結果を変数に代入するvar input = getLine()というようなコードは変数を宣言するです。Haskellでもinput <- getLineというような文を書くことがありますが、これは実はgetLine >>= (\input -> ... )というような式のシンタックスシュガーで、実はgetLineという式と(\input -> ...)という関数を演算子>>=に適用するという式です。<-という記号は演算子ではなく構文であり、inputという変数を宣言して代入しているというより、実はinputはただの無名関数リテラルの引数だったりします。

真偽値 Bool とか文字型 Char とかがある.Floatが32bit浮動小数点数,Doubleが64bit浮動小数点数.[Char] は文字のリストで String と等価.

ところでRustの StringVec<u8> なのだがそんなパフォーマンスが悪いとかいう話は聞いたことがないが,ベクタとリストの違いとか,Char だからとかそういうのがあるんだろうか.遅延評価だし

IntInteger の違い.Int の固定長整数 (最低30bit以上) ってすっごいフランクな仕様だな…
というか IntInteger で違う型なのやさしくない.Rustの i32 usize f64 が一番.

(Int, Char) みたいなタプル型も作れる.関数型は Int -> Int とか Int -> Int -> Int とか.Elmで概念は知っているが複数引数関数はカリー化されている,と.それよりTypeScriptだと (n: number) => (m: number) => number みたいに引数名が要るのでうっかり書きそうになる.

トップレベルに変数を置く.関数型だと再代入できないので束縛って呼んだりするアレ.

x = 42
main = print x

型注釈2.変数の型注釈というより値の注釈っぽく感じる.

x = 42 :: Int

やっぱこれ main :: IO () と酷似してるけど main は本当に関数なのか?もしくは x も関数なのか?関数と変数の境界線がない?

よく考えると遅延評価って値が実際に評価されるまでコードが走らないんだったら x が使われるタイミングでコードが実行されるんかな.だとしたらなんか関数っぽいな.

実際に評価されるタイミングまでコードの実行が遅延される,という性質を関数が持ってる,とも言えるね.

失礼だけど 0xe38182 みたいな16進リテラルがあることが驚きというか,あのHaskellにそんなお低レイヤーな表現方法を実現するためのものが言語組み込みで入れられているなんて…というか,似合わないなという感じがする.

文字列に "\LF" みたいな制御文字があるところとかも.

JavaScriptの民,絶対シングルクォートが文字型の言語で 'foobar' とかやって怒られたことがある.

リストは内部的には要素が次の要素へのポインタを保持する形式になっている.配列はたしかスタックにまとめてバーッって積まれてるはず.たしかRustではそうなのでコピーが発生する型だったはず.ベクタは知らんが借用とか参照が登場するのでヒープに置かれてるんじゃないかなあ.

[1..4][1,2,3,4] と等価になるのはわかる.よくRangeみたいなので使われる構文だからな.だが [1, 3..9][1,3,5,7,9] になるのはどういうことだァ~~~ッ!?わざわざ1と3の間隔を確認してそのとおりに展開してるっていうのか~~~~!?

[1,2,4..12] みたいなのは構文エラーになった.どうやら [a,b..n] とするとaとbの差を維持して一次関数的にうにょーんってnまで伸ばしてくれるという構文らしい.

これCharにも使えて [a..d] とすると [a, b, c, d]になるらしい.それでは ['A'..'z'] するとどうなるか?

Prelude> ['A'..'z']
"ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz"

ほう…

Prelude> ['\NUL'..'\DEL']
"\NUL\SOH\STX\ETX\EOT\ENQ\ACK\a\b\t\n\v\f\r\SO\SI\DLE\DC1\DC2\DC3\DC4\NAK\SYN\ETB\CAN\EM\SUB\ESC\FS\GS\RS\US !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\DEL"

ASCII全制覇.

head 関数はリストの先頭を返すらしい.

Prelude> head []
*** Exception: Prelude.head: empty list

より実践的なのは

main = do
  s <- getLine
  putChar (head s)

して何も入力せずEnterするのがいいかな.

ところで

putChar (head s)

これがクソダサいのだが,

putChar $ head s

こう書けるらしい.行末まで囲ったのと同じ効果らしいが微妙に使えないシーンとかがあるのでなにかありそう.

Application operator. This operator is redundant, since ordinary application (f x) means the same as (f $ x) . However, $ has low, right-associative binding precedence, so it sometimes allows parentheses to be omitted; for example:
f $ g $ h x = f (g (h x))

関数適用の演算子で,(f $ x)(f x) は同じだけど,$ 演算子は優先度が低く右結合という特徴があるので,括弧を消すのに使える時がある.

[1, 2, 3] !! 2 これで (0から数えて) 2番目の要素,すなわち3を返すらしい,らしいのだが,見た目が物騒すぎるだろ…nullかもしれない値をヌルポ上等でnon-null値として扱わせるときのやつだよこれ…

Prelude> [1, 2, 3] !! 65535
*** Exception: Prelude.!!: index too large

Haskellには最初からモナドが載ってたわけじゃないらしいので,たぶん初期初期の初期,Maybe すらない時期に設計されたんだろうなあ.

head tail init last の表作ってみた.

先頭 末尾
head init の要素
tail last を除いたリスト

1~6のリストの要素をx に束縛して x*x したリストを返す式.これがfor内包表記ってやつ?

[x * x | x <- [1 .. 6]]

JavaScriptでいうと

[1, 2, 3, 4, 5, 6].map(x => x * x)

って感じ.

, の後ろにガードを付けられる.これは1以上6以下の偶数の平方を昇順に並べたリスト,と表せるかな.

[x * x | x <- [1 .. 6], even x]

JSって奇数偶数の判定関数ないのな…Number.isEven とか Math.isEven とかあるもんだと思ってた…

[1, 2, 3, 4, 5, 6].filter(x => x % 2 === 0).map(x => x * x)

Haskell Language Serverのすごいところは,そんなこんなで even を使う発想に至らず,

[x * x | x <- [1 .. 6], x `mod` 2 == 0]

こうやって書いたとしても,

Use even
Found:
  x `mod` 2 == 0
Why not:
  even x

こうやって修正ヒントを出してくれる.すげえ,めっちゃ初心者にやさしいじゃん.

タプルの各要素は異なる型でもよい.

0個の要素をもつタプル型 () は unit 型と呼ばれる.IO () とかですでに出ている.値が1個の型なので 1 型と呼んだりもするっぽい.

サンプルコードこれ非合法じゃん.

fst (1, 'a', "ABC")
<interactive>:5:5: error:
    • Couldn't match expected type ‘(a, b0)’
                  with actual type ‘(Integer, Char, [Char])’
    • In the first argument of ‘fst’, namely ‘(1, 'a', "ABC")’
      In the expression: fst (1, 'a', "ABC")

型定義

fst :: forall a b. (a, b) -> a

読み方がよくわからないんだが任意の型 a b について,タプル (a, b) を取って a を返すってことでいいんだろう.

3要素以上のタプルはパターンマッチ (?) で分解しろということらしい.

(_,_,x) = (1,2,3)

JSの分割代入っぽいけど分割代入がパターンマッチっぽいとの見解も出されており決着がつく見通しはない.

Haskellをなんかヤバい化物みたいに思われがちだろうが,普通の言語と同じように + - * / という一般的な演算子が使える.よかった,Haskellも普通の言語なんだね!ところでこれらは限りなく関数に近い存在である.カーソルを乗せると「関数ですよ?」みたいな顔したシグネッチュアー[1]が出てくる.

+ :: forall a. Num a => a -> a -> a

普通に型クラス出てくるし…

脚注
  1. signature ↩︎

関数ですよどころか,演算子を () で囲うと関数になってしまう.

main = do
  print $ (+) 3 2

これかなり便利で,map みたいな関数を受け取る関数とかだとこう書けてしまう.

map (* 3) [2, 3, 4]

JSでは

[2, 3, 4].map(x => x * 3);

と書くことになる.

逆に p -> q -> r という感じで引数を2つとる関数をバッククォートで囲み,中置演算子っぽく使うこともできる.

add :: Num a => a -> a -> a
add x y = x + y

main = do
  print $ 4 `add` 3

ちなみにGHCが最強すぎて add のシグネチャが勝手に型クラスつきで推論され,さらにHLSが最強すぎてVSCode上で実装の上側にちっちゃく出てきてそれをクリックするとコードとして実体化する.ほんとにこれは実体化としか言いようがない出方で笑える.

冪乗が ^ ^^ ** と3つ存在するんだが,

** :: forall a. Floating a => a -> a -> a
^ :: forall a b. (Num a, Integral b) => a -> b -> a
^^ :: forall a b. (Fractional a, Integral b) => a -> b -> a

数値関連の型クラスだけで Floating Num Fractional Integralと4つもあるの笑えるな.

否定に ! を使わない文化.Bool -> Boolnot 関数があったり不等価が /= だったりする.

. 関数合成演算子.なんか数学的には射?写像?の合成になるらしいが…

square x = x * x

twice x = x + x

twiceSquare = square . twice

squareTwice = twice . square

main =
  do
    print $ twiceSquare 10
    print $ squareTwice 10

g . f は数学の関数合成 g\circ f にそっくりそのまま対応している.数式で表すと

(g\circ f)(x) := g(f(x))

となり,Haskellコードでは

gf1 = g . f
gf2 x = g $ f x

と表現できる.要するに関数適用の順番は f が先で g が後,もっと言うと 文字送りの方向と逆

ラムダ式 \x -> expr ももちろんある.これはElmと同じ.ドキュメントが「バックスラッシュをよく見ているとラムダ λ に見えてきて,Elmの背後にある数学の理論を想起させますね!」[1] みたいなことを書いていたので記憶に残っている.

脚注
  1. うろ覚え ↩︎

例によって \x -> x + 3 とか書くと (+3) への修正提案が出るので使う場面が少なめ.多引数関数があるせいで arr.map(v => console.log(v)) と書かざるを得ないどっかの言語がアレなだけだと思われるが…

個人的なJavaScriptへの恨みというか,アロー関数の () {} 省略の規則が厳しくて書きづらいのと,arr.map(({ hoge まで書いた時にアロー関数とわからないせいで分割代入プロパティ hoge がちゃんと推論されないのはどうにかならなかったのか.

関数定義でもパターンマッチができる.

f 0 = "Zero"
f 1 = "One"

オーバーロードっぽい.

ここで「オーバーロードは引数の型に対して実装が切り替わるシステムなんだから値に対する分岐を行っているだけのこれは違うだろ」というマサカリを振り上げて助走に入っているみな様がたはもちろんまったく正しいのだが,TypeScript にはリテラル型というのがあるので x: 0x: 1 で返り値の型を分けるようなことができてしまうのだ.逆に TypeScript の関数オーバーロードは型の上だけであって実装を複数持たせてディスパッチーとかできないので,とりうる引数の型ぜんぶが突っ込まれてunion型になってる1つっきりの実装を操らなければならない.

こんな感じ.一番下のアノテーション部分推論してほしい.

function f(x: 0): "Zero"
function f(x: 1): "One"
function f(x: 0 | 1): "Zero" | "One" {
  return x ? "One" : "Zero"; // 0のfalsy性を悪用
}

あとこれ "One""Zero" をフリップしても怒られない.どちらかというとJSのイカしたAPIに無理やり型をつけるための機能であってTSで使うのは危ないという認識.

そんなことはどうでもいいな.Haskellに話を戻す.さっきの f,あれ 01 にしか定義されてないくせに型推論では f :: (Eq a, Num a) => a -> [Char] になっていて 2 とか -3.141592653とか入れるバカを止められない.

main = print $ f 3 -- 💣 Non-exhaustive patterns in function f

上でリンク貼った記事の「パターンマッチで落っこちる」というのはおそらくこれ.

とりあえずエラーを消すには,変数としてキャプチャするパターンを増やせばよい.

f n = "Out of range"

これではあまりにHaskellらしくないね.じゃあちょうどいいタイミングなので Maybe を導入する.

Maybe はデータ構造.Maybe a は「a型の値がある,またはない」を表す.Javaで失敗をあらわすのに null を使うのと同じようなものだが,T 型にアクセスしたら null 踏んで java.lang.NullPointerException で死ぬなんて間抜けなことは発生しない.虚無値である可能性を型として扱えるからだ.KotlinやTypeScriptやSwiftやRustといった最近の言語もそうだ.そして Haskell はそれらより1まわりぐらい世代が古いにもかかわらず虚無値を型として扱うことを実現していたというわけ.

Haskellの Maybe a 型は Just aNothing という値のどちらか.失敗したら Nothing を返すというのがよくある使い方.

f 0 = Just "Zero"
f 1 = Just "One"
f n = Nothing

これで 50000 とか渡すのがいても問題ない.

パターンマッチと再帰を組み合わせることもできる.これがやばくて,たとえば階乗を求める関数は

fact 0 = 1
fact n = n * fact (n - 1)

と書ける.数式による定義は

n! = \begin{cases} 1 & (n = 0) \\ n(n - 1)! & (\text{otherwise}) \end{cases}

である.JavaScriptだと

const fact = (n: number) => n === 0 ? 1 : n * fact(n);

となる.

パターンマッチと似てるようで違う ガード条件 というのもある.条件式を書ける.

moreThan5 n
  | n > 5 = "greater than 5"
  | n < 5 = "less than 5"
  | otherwise = "equals 5"

Kotlin の when 式とかだとかなり自由に条件書けた気がしてこれに近いかも.

ここで出てくる otherwiseTrue が入っているだけである.

moreThan5 n
  | n > 5 = "greater than 5"
  | n < 5 = "less than 5"
  | True = "equals 5"

このように True に書き換えてもよいが,例によって修正提案が出される.

otherwise という構文を導入するのではなく定数で対処するのがよくできてるなあと思った.

ガードのどれにもマッチしない場合 Non-exhaustive patterns in function moreThan5 で実行時エラー.

moreThan5 n
  | n > 5 = "greater than 5"

main =
  print $ moreThan5 5

リスト分解のパターンマッチもある.

func (x : xs) = do
  print x
  print xs

main =
  func "2345"

めんどくさくて触れるの端折ってしまってたのだけど,これは : でリストを結合するのの逆.

main =
  print $ '4' : "foobar"

@ で引数を複数の形式で受け取れる…というかなんというか.文字列全体を full に,さらに先頭の文字を
x に,残りの文字列を xs に受け取る.

func full@(x : xs) = do
  print full
  print x
  print xs

main =
  func "foobar"

let で変数に値を束縛 (bind) する.もちろん不変.

f n = do
  let m = n + 3
  m

main = print $ f 3

関数の本体のシンタックスがよくわからん.

let-in でスコープを切れる.

area r =
  let pi = 3
   in do
        r * r * pi

違いがわからん.

area r = do
  let pi = 3
   in r * r * pi
area r = do
  let pi = 3
   in do r * r * pi

if condition then positive else negative という式もある.else if はもしかしてない?

case でどこでもパターンマッチ.

f n = case n of
  0 -> Just "Zero"
  1 -> Just "One"
  _ -> Nothing

https://github.com/tc39/proposal-pattern-matching

これの case-when って Haskell 由来なんですね~(嘘をつけ) (実際は互換性の問題でwhenを新しく予約語として追加します!!とかできないからだと思う)

where を使うと定義を後ろに書ける.これは半径rの鉄の質量を求めるだけの中二病コード.

mass r = v * rho
  where
    rho = 7874
    v = volume r
      where
        volume r = r * r * r * pi * 4 / 3
          where
            pi = 3.1415

main = print $ mass 1

みんな大好き直和.これって新しいリテラルとそれらを値として持つ型 Color を定義してるって説明すればいいのかな?

data Color = Cyan | Magenta | Yellow | Kuro

コンストラクタは値を持てる.直積.

data Color = Rgb Int Int Int 

直積の直和.

data Color = Rgb Int Int Int | Hsl Int Int Int

型クラス Show を突っ込むと文字列表現を持ててよい.用語としては導出とかでいいのかな?

data Color
  = Rgb Int Int Int
  | Hsl Int Int Int
  deriving (Show)

main = print $ Rgb 255 255 255

Nixの derivation って derive の派生か~…そして deliver と若干混同してた.あとRustにも #[derive Clone] みたいな感じでトレイトを導出するのがあるな.

rgb値は0~255の範囲内なので,それに従うよう適当に引数をバリデーションしてみる.いわゆるスマコン (smart constructor).

data Color
  = Rgb Int Int Int
  | Hsl Int Int Int
  deriving (Show)

rgb r g b =
  if valid then Just $ Rgb r g b else Nothing
  where
    valid = foldl validate True [r, g, b]
      where
        validate c n = c && n >= 0 && n <= 255

main = print $ rgb 255 255 (-3) -- Nothing

コンストラクタを1つにすると名前付きタプル的なのになる.関数の引数で普通にパターンマッチして足して返す例.

data Point = Point Int Int

addPoint (Point x1 y1) (Point x2 y2) = Point (x1 + x2) (y1 + y2)

instance TypeClass Type whereType を型クラス Show のインスタンスにするというかなんというか.

instance Show Point where
  show (Point x y) = "x:" ++ show x ++ ", y:" ++ show y

main = print $ show $ addPoint (Point 10 30) (Point 20 20)

deriving だといい感じに出力されるが,instance だと詳しく表示できるよねっていう感じだと思う.

newtype はパイロット適性のあるやつのことではなく,1つのコンストラクタと1つのフィールドをもつ新しい型を生成する.data より効率的らしい.

newtype Meter = Meter Int

type は型エイリアスで,既存の型の別名を定義する.String[Char] だったりするのもあれ.

type Person = (Name, Age)
type Name = String
type Age = Maybe Int

型クラスを定義して使う例.

class PrettyStr a where
  prettyStr :: a -> String

instance PrettyStr Point where
  prettyStr (Point x y) =
    "Point:\n\
    \  x: "
      ++ show x
      ++ "\n\
         \  y: "
      ++ show y

main = putStrLn $ prettyStr $ addPoint (Point 10 30) (Point 20 20)

文字列内での改行が,行末と次行頭に \ なのがへえって感じ.

ghci で :i TypeClass とすると型クラスの定義が見える.

Prelude> :i Functor
class Functor (f :: * -> *) where
  fmap :: (a -> b) -> f a -> f b
  (<$) :: a -> f b -> f a
  {-# MINIMAL fmap #-}
        -- Defined in ‘GHC.Base’
instance Functor (Either a) -- Defined in ‘Data.Either’
instance Functor [] -- Defined in ‘GHC.Base’
instance Functor Maybe -- Defined in ‘GHC.Base’
instance Functor IO -- Defined in ‘GHC.Base’
instance Functor ((->) r) -- Defined in ‘GHC.Base’
instance Functor ((,) a) -- Defined in ‘GHC.Base’

Functorfmap をもつ.最初 fmapflatMap だと思ってたのだが違うようだ.functor map の略っぽくて,どうやら標準の map はリストにしか使えないらしい.ハァ~!?

要約すると

class Functor (f :: * -> *) where
  fmap :: (a -> b) -> f a -> f b

ここで fmapf a を受け取って f b を返している.具体化するなら [Int] -> [String] のように.ここで Int String のように構造の中身は変わっているが,リスト [] 構造であることは変わっていない.リストは f に相当する.

fmap fn で簡単に部分適用できるのが素晴らしいところ.データ構造を最後に受け取る形のカリー化になってるのはこういう理由.

Prelude> fmx2 = fmap (* 2)
Prelude> fmx2 $ Just 43
Just 86
Prelude> fmx2 $ Nothing
Nothing

ところでとほほの入門だと

Prelude> fmx2  (4, 7)
(4,14)

これは (8,14) になるはずなのだが,なんか変だ.とほほ入門はタプルに弱いのか?

fmap fn ffn <$> f とも書ける.Haskellお得意で初心者殺しの謎演算子.でもこれ fn `fmap` f でよくないか?

次,Applicative.なんかとほほではアプリケイティブって書いてるけど英語発音に従うならアプリカティブが正しそう.

Prelude> :i Applicative
class Functor f => Applicative (f :: * -> *) where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b
  GHC.Base.liftA2 :: (a -> b -> c) -> f a -> f b -> f c
  (*>) :: f a -> f b -> f b
  (<*) :: f a -> f b -> f a
  {-# MINIMAL pure, ((<*>) | liftA2) #-}
        -- Defined in ‘GHC.Base’
instance Applicative (Either e) -- Defined in ‘Data.Either’
instance Applicative [] -- Defined in ‘GHC.Base’
instance Applicative Maybe -- Defined in ‘GHC.Base’
instance Applicative IO -- Defined in ‘GHC.Base’
instance Applicative ((->) a) -- Defined in ‘GHC.Base’
instance Monoid a => Applicative ((,) a) -- Defined in ‘GHC.Base’

Functor f =>Functor の派生クラスってことなのかもしれない.pure<*> をもつ.

pure は適当な値を受け取って構造に突っ込む関数.Int -> Maybe Int 的な.というかこれがないFunctorってありえるのか?

<*> は構造に入った関数を受け取って構造に入った値に適用する.Maybe (Int -> String) -> Maybe Int -> Maybe String 的な.

Prelude> Just show <*> Just 3
Just "3"
Prelude> Nothing <*> Just 3
Nothing
Prelude> Just show <*> Nothing
Nothing
Prelude> Nothing <*> Nothing
Nothing

ほう.

Prelude> [(*2), (*3)] <*> [1,2,3]
[2,4,6,3,6,9]

この挙動が「ListをFunctorのインスタンスにすると必然的にこうなる」のか「プログラミング的に一番便利なこの挙動を恣意的に選んで <*> を実装している」のかが気になる.
<*> :: [Int -> Int] -> [Int] -> [Int] だもんな.

次,ついにモナド.

Prelude> :i Monad
class Applicative m => Monad (m :: * -> *) where
  (>>=) :: m a -> (a -> m b) -> m b
  (>>) :: m a -> m b -> m b
  return :: a -> m a
  {-# MINIMAL (>>=) #-}
        -- Defined in ‘GHC.Base’
instance Monad (Either e) -- Defined in ‘Data.Either’
instance Monad [] -- Defined in ‘GHC.Base’
instance Monad Maybe -- Defined in ‘GHC.Base’
instance Monad IO -- Defined in ‘GHC.Base’
instance Monad ((->) r) -- Defined in ‘GHC.Base’
instance Monoid a => Monad ((,) a) -- Defined in ‘GHC.Base

>>= >> return と,普通の手続き型から来ると頭爆発しそうな名前が揃っている.リダイレクト演算子かな?

今気づいたけど {-# MINIMAL (>>=) #-} って「>>= が定義されてればOK」ってことを意味しているのかな?

return :: a -> m a って pure :: a -> f a と何が違うんだ?それはともかく >>= は bind と読む.構造にくるまった値 m a と生の値を構造にくるむ関数 a -> m b を受け取って m b を返す.これ flatMap だな.さっきのJSモナド入門では Array.prototype.flatMap がモナドとかなんとか言ってたけど,Array<T>.flatMap((v: T) => Array<T>): Array<T> ってことだしな.

Maybe を生やす.Maybe と名前を重ねたくないので Option という型を定義する.Rustyだね.

data Option t = Some t | None deriving (Show)

Option Int を2個受け取って,双方がSomeだったら足してSomeで返す.片方でもNoneだったらNone.

add :: Option Int -> Option Int -> Option Int
add ox oy = case ox of
  None -> None
  Some x -> case oy of
    None -> None
    Some y -> Some (x + y)

main = do
  print $ add (Some 3) (Some 4)
  print $ add (Some 3) None
  print $ add None (Some 4)
  print $ add None None

長すぎてだめ.

FunctorとApplicativeを実装して,

instance Functor Option where
  fmap f (Some a) = Some (f a)
  fmap _ None = None

instance Applicative Option where
  pure a = Some a
  (<*>) (Some f) (Some a) = Some (f a)
  (<*>) None _ = None
  (<*>) _ None = None

Monadも実装する.

instance Monad Option where
  return a = Some a
  (Some a) >>= f = f a
  None >>= _ = None

するとこうなる.\x -> ...\y -> ...Int -> Option Int に相当する. returnSome と同じだからそれでいいじゃんって気もするけど…

addm :: Option Int -> Option Int -> Option Int
addm ox oy = ox >>=
  \x -> oy >>=
    \y -> return (x + y)

さらにdo記法という構文糖衣を使うことで、次のように書くことができる。

とあるように (?) do を使うとこれがもっと簡潔になる.

addd :: Option Int -> Option Int -> Option Int
addd ox oy = do
  x <- ox
  y <- oy
  return (x + y)

おお…?見事な伏線回収というか,<- は再代入してるのに見せかけた無名関数っていうのはこういうことか.そして return がただの関数のくせにすごい return っぽい見た目してるのがむかつくな…

x <- ox には,「oxNone だったら None を返して終了」という意味が含まれていて,含まれているから x が生の Int になるってことだが,これ Rust で見たぞ.None Err a だったらそれを返して関数を終わる ? ってやつ.

ほらやっぱりそうじゃん!

fn addd(ox: Option<i32>, oy: Option<i32>) -> Option<i32> {
    let x = ox?;
    let y = oy?;
    Some(x + y)
}

fn main() {
    println!(
        "{:?}\n{:?}\n{:?}\n{:?}",
        addd(Some(3), Some(4)),
        addd(Some(3), None),
        addd(None, Some(4)),
        addd(None, None)
    );
}

で,さらに,Wikipeたん曰く,

この種の操作は頻出なので、Haskellの標準関数(liftM2)が用意されている。

という.名前がダサい.

addl :: Option Int -> Option Int -> Option Int
addl = liftM2 (+)

草.

(上記のdo記法を使ってliftM2の定義を書いてみよう)

などと仰せられる.個人的に,Wikipeたんが偶にフレンドリーな口調を見せるのに萌えるので,実装してみる.

riftM2 f mp mq = do
  p <- mp
  q <- mq
  return (f p q)

addr :: Option Int -> Option Int -> Option Int
addr = riftM2 (+)

できました.

なんか fmap 使えって修正提案出された.

riftM2 f mp mq = do
  p <- mp
  f p <$> mq

return のがわかりやすい気もする.

とほほの入門はモナドで終わり.あとはてきとうな文献をあたることにする.さっきのWikipediaがまだまだあるので読む.まずは IO.

putStrLn が出力するのではなく,これは「文字列を出力することを示す値」を返し,それを main が返すとGHCが出力する.非純粋な部分を関数の外側に追いやるのを言語全体に適用しただけともいえる.

putStrLnString -> IO () すなわちとくに何も得ることはない.getLineIO String なので >>= (\name -> ...)name は受け取った文字列.これを putStrLn に突っ込む.

main = putStrLn "Tell me your name:" >>= (\_ -> getLine >>= (\name -> putStrLn $ "Nice to meet ya, " ++ name))

do の偉大さよ.

main = do
  putStrLn "Tell me your name:"
  name <- getLine
  putStrLn $ "Nice to meet ya," ++ name

>> について触れてなかった.putStrLn "foo" >>= \_ -> getLine の無名関数の引数 _ が無駄なので,putStrLn "foo" >> getLine と書ける.左の結果を捨てる.

main = do
  putStrLn "Tell me your name:" >> getLine >>= \name -> putStrLn $ "Nice to meet ya," ++ name

x <- ff >>= \x ->. .. の糖衣構文」というのは正しいが説明不足で,「ff >> ...」である,という面があるというかなんというか.

状態モナドがわからんな…

よく見たらピュアスクじゃねーか!PureScriptはHaskellとよく似てるようにみえて用語変更とかも普通に多いのでそのまま読むことはできない.>>> 演算子 (左から右への関数合成) とかはHaskellのPrelude (組み込み的な意味合い) にないのでimportが必要.

import Control.Category ((>>>))

このPureScriptコードは,

transit :: Int -> Int
transit
    = (_ + 1)    -- 前の状態に +1 した新しい状態を返す関数
  >>> (_ + 2)    -- 前の状態に(以下略)
  >>> (\_ -> 3)  -- 前の状態に関係なく新しい状態を 3 にする関数
  >>> (_ + 4)    -- 前(以下略)
transit :: Int -> Int
transit =
  (+ 3)
    >>> (+ 2)
    >>> const 3
    >>> (+ 4)

こうなる.目立つ違いは演算子の部分適用の文法ぐらい.

これに初期値 3 を渡す,つまり transit 3 とすると初期値の 3 から 4 → 6 → 3 ときて最後に 7 で終わる.

これの途中にたとえば even とかを挟んだり,even の結果や状態に応じてなにかしたりするのはできない.even :: Int -> Bool[1] なので.

脚注
  1. 厳密にはもっと抽象的だけど今はIntしか扱ってないので具体化して扱う ↩︎

タプルにして,Intの流れる本流と別に値を持つところを作ればいいと.ここ思いつくのがもう天才では?

import Control.Category ((>>>))

transit :: (a, Int) -> ((), Int)
transit =
  (\(_, s) -> ((), s + 3))
    >>> (\(_, s) -> (even s, s))
    >>> (\(b, s) -> ((), if b then s `div` 2 else s))
    >>> (\(_, s) -> ((), s + 4))

main = do
  print $ transit ((), 3)

PureScriptはレコード (JSのオブジェクトに対応するらしい) を使ってタプルっぽいのを作る.Haskellでは普通にタプルを使えばよい.

でもなんだか同じようなコードが何度も出てきたり、やたらとタプタプしてかっこ悪いです。
なので意図が伝わりやすいよう名前をつけてかっこよくしてみましょう。

プログラミングにおいてコードがかっこいいとは,繰り返しが少なく意図が伝わりやすいということである (俺, 2021).

State を定義する.Stateといってもただのタプルを返すだけの関数だが.

type State s a = s -> (a, s)

バインド bind も定義する.

m `bind` f = m >>> \(a, s) -> f a s

便利な関数たち.

modify f s = ((), f s) -- 状態を変更

gets f s = (f s, s) -- 現在の状態を使ってなんかする

get s = (s, s) -- 現在の状態を得る

put s = const ((), s) -- 状態を設定

lift m s = (m, s) -- なんかする

execState m = m >>> snd -- 初期状態を渡して最後の状態を得る

タプル度が減った.

transits :: State Int ()
transits =
  modify (+ 3)
    `bind` (\_ -> gets even)
    `bind` (\b -> modify (\s -> if b then s `div` 2 else s))
    `bind` (\_ -> modify (+ 4))

さらに do でも書ける (これどうなってるんだ?基準が謎).

transitd :: State Int ()
transitd = do
  modify (+ 3)
  (b, _) <- gets even
  modify (\s -> if b then s `div` 2 else s)
  modify (+ 4)

せっかくなのでちゃんと Monad のインスタンスであることを確かめたい.

typeはインスタンス化できないのでnewtypeにする
newtype State s a = State (s -> (a, s))

State は前の状態を受け取って (処理の結果, 新しい状態) を返す関数!」と300回叫んでから以下のコードを書くことでFunctorを実装する.まえまえからの流れのおかげで元記事とタプルの順番が入れ替わっている.

instance Functor (State s) where
  fmap f (State g) = State $ \s ->
    let (a, s') = g s
     in (f a, s')

つか識別子に'使えるのな.ともかく fmap(a -> b) -> (s -> (a, s)) -> (s -> (b, s)) ((a -> b) -> State s a -> State s b) なので.関数 f が適用されるのはタプルの左,実行結果の部分だけ.

g これ何?なんで呼んでんの?

Monad.

instance Monad (State s) where
  (State g) >>= m = State $ \s ->
    let (a, s') = g s
        (State e) = m a
     in e s'

いまさらだけど,Stateを実行する (関数呼び出しする) にはコンストラクタを剥がすしかない.剥がして生の関数を返す runState 関数がないと使えない.これは newtype にしたことによるあれ.

runState :: State s a -> (s -> (a, s))
runState (State g) = g

さらに (s, a) の片方だけ必要な場合に使う関数を定義する.

evalState f = runState f >>> fst
execState f =  runState f >>> snd

いま理解しようとがんばって do やりまくってるんだが,>>= のほうが挙動はわかりやすいかもしれない.

ややこしくなってる理由がわかった.newtype 化によって起きた実装の変化にまずついてこれてない.

ここまで展開すればどこがどうなってるかわかる.>> すら使わない徹底ぶり.

trb =
  State (\s -> ((), s + 3))
    >>= \_ ->
    State (\s -> (even s, s))
    >>= \b ->
    State (\s -> ((), if b then s `div` 2 else s))
    >>= \_ ->
    State (\s -> ((), s + 4))

ここで \_ \b \_ とラムダ式の引数が並んでる部分,ここが処理結果 a を受け取る部分.3つある \s が前回の状態を受け取る部分.

>>= の右オペランドは a -> State s b つまり a -> s -> (b, s)\_ \ba 型,\s 部分が s 型,((), s + 3) とかの部分が (b, s)

ようやく「完全に理解した」のあたりまできた.というか >>= の挙動を完全には理解できてなかったというか,State s がもつモナドな構造がどのような形をしているかを理解してなかった.当たり前だけど >>=State s a -> (a -> State s b) -> State s b だからな,2つの式を結合して新しいStateにする.

「なんでStateの g 呼んでんの?」という問いへの答えは「g の返り値を別の関数に渡すことでStateと関数を合体させて新しいStateを作るから」とかかな?

状態モナドは完全に理解したけど使うのはわりと難しそうだとおもった.というか fmap<*> すっ飛ばしてしまったし.まあ fmap は既存のStateの a 部分をいじるってだけだけど.

trd = do
  set (+ 3)
  isOdd <- not <$> gets even

  set (\s -> if isOdd then s `div` 2 else s)
  set (+ 4)

Applicativeは知らん.

おひさ.きょうはIdモナドについて書く.Idモナドが一番基本的なモナドらしい.たぶん.

1個のコンストラクタが1個の値を保持する.それだけ.

newtype Id a = Id a

State どころか Maybe より簡単なのでインスタンスを実装するのはまったく大変がなく自明自明アンド自明.

instance Functor Id where
  fmap f (Id a) = Id $ f a

instance Applicative Id where
  pure = Id
  (<*>) (Id f) (Id a) = Id $ f a

instance Monad Id where
  (Id a) >>= f = f a

使いみちが全然思いつかないんだけど Monad を要求する関数に生の値を渡したいときに使えるのかな.

Eitherモナドでも生やすか.知ってるけどインスタンス実装は知らないし.Maybeわかればいけるだろ.

Either が Prelude (組み込みで入ってるあれ) に入ってるのでそのままやると衝突する.衝突した定義自体ではエラーにならず Either を使おうとしたときに ambiguous (曖昧) なんとかでエラーになるので,利用する時に衝突を回避する方法があるのだろう.Result-Ok-Err はもちろん Rust からパクった.

data Result e a = Ok a | Err e

instance Functor (Result e) where
  fmap f (Ok o) = Ok $ f o
  fmap _ (Err e) = Err e

余裕と思ってたけど Result は当然ながら型引数を2つもつのでカインド * -> * を要求する Functor にはそのままでは使えない.カインドというのは型が型引数をとってなんか返すみたいなアレだと思う.Result e a のカインドは * -> * -> *.関数みたいに部分適用できる.すなわち Result e のカインドは * -> * ってわけ.へ~すごい

Result の型定義で e aa e か,もっと一般化して言うと Ok 側と Err 側の2つの型引数はどっちを先に取ったほうがいいんだろうという疑問が浮かんだ.結論から言うと Ok 側があとになる.

そもそも Functor の定義は以下のようなものだった (ghci にて :i Functor してみよう).

class Functor (f :: * -> *) where
  fmap :: (a -> b) -> f a -> f b
  (<$) :: a -> f b -> f a
  {-# MINIMAL fmap #-}
        -- Defined in ‘GHC.Base’

抽象的なものは具体例にたくさん触れることが理解につながる気がする.具象どうしで共通する物事を取り出したのが抽象だし.Maybe State Id IO などを学んでいるのもそれらを共通化するモナドを理解するためだったわけで.なので一番具体的なものとして Maybe Int を考えてみよう.fmap f という関数は Maybe に対しては「Just a だったら中身 af を適用した結果を Just に包んで返す」「Nothing だったら Nothing を返す」という働きをする.

Result e a についても同じことを考えると,Ok a が正常値で Err e が異常値として扱うなら fmap ffa -> b で「Ok a だったら a を剥がして関数 f に適用して Ok に包んで返す」というほうがうれしいはずだ.これが理由.Err e だった場合のみの処理ももちろん欲しいけど,それは Ok a と比べてどっちを fmap に割り当てるべきか?ということだろう.

なんか Functor だけじゃなくて Monad 含めて e a のが良いと思ってたみたいで,Functor だけで説明するとめっちゃ根拠が弱く感じる…Monad までやって do が出てきたら絶対にこっちのほうがいいってなるから.

というかまだ早かったかな.急に不安になってきたぞ.

data Kekka g b = Yoshi g | Ashi b

instance Functor (Kekka e) where
  fmap f (Yoshi g) = Yoshi g
  fmap f (Ashi b) = Ashi $ f b

Applicative の定義を見る.実装で関数を表す f と型引数で関手を表す f が交じって読みづらいのなんとかならんかな.

class Functor f => Applicative (f :: * -> *) where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b
  GHC.Base.liftA2 :: (a -> b -> c) -> f a -> f b -> f c
  (*>) :: f a -> f b -> f b
  (<*) :: f a -> f b -> f a
  {-# MINIMAL pure, ((<*>) | liftA2) #-}
        -- Defined in ‘GHC.Base’

fResult e で具体化して,インスタンスの持つべき関数がどんなシグネチャなのかを見てみよう.

pure
(<*>) :: Result e (a -> b) -> Result e a -> Result e b

こうするとかなり理解しやすい.

実装終わり.

instance Applicative (Result e) where
  pure = Ok
  (Ok f) <*> (Ok a) = Ok $ f a
  (Err e) <*> _ = Err e
  _ <*> (Err e) = Err e

これ (Err e) <*> (Err e) のときはどっちを返すべきなんだろうか?多重定義が定義順にマッチされるとしたらこの場合は関数側の値が取られそうだけど…

モナドに行く.>>= の型は

(>>=) :: Result e a -> (a -> Result e b) -> Result e b

a -> Result e b は失敗する可能性のある関数なのが大切だ.ちょっといまさら気味だけど a -> Result e b は失敗する可能性のある関数なんだよな.そんで全体の返り値が Result e b になっているのが重要というかなんというか.これを繋げて Result e b -> (b -> Result e c) -> Result e c みたいなこともできるんだよな.ここで Result e を返す関数を繋げてるのに Result は1段しかないというところが重要というか.

それはともかく,

instance Monad (Result e) where
  return = Ok
  (Ok a) >>= f = f a
  (Err e) >>= _ = Err e

使ってみるか.じゃあまずリストの最初と次を入れ替える関数.

flip' :: [a] -> Result [a] [a]
flip' [a, b] = Ok [b, a]
flip' c = Err c

先頭を次で割る関数.次が0だったり数が2じゃなかったらErr

foldiv :: (Fractional a, Eq a) => [a] -> Result [a] a
foldiv [x, 0] = Err [x, 0]
foldiv [x, y] = Ok $ x / y
foldiv a = Err a

繋げるとこんな.

flfol a = do
  a <- flip' a
  foldiv a

ここで a <- で取得されている aResult e aa 型に相当している.Result a e とすると do で代入っぽいことしたときにエラー値が入ってきてしまう.

都合のいいことにこれをまとめた関数が用意されている.

import Control.Monad ((>=>))

flfol2 = flip' >=> foldiv

自分で実装することで理解を深める.

(>=>) :: (Monad m) => (a -> m b) -> (b -> m c) -> a -> m c
f >=> g = \a -> do
  b <- f a
  g b
ログインするとコメントできます