Haskell の学習ログ (基礎~Stateモナドぐらいまで)
楽しそうなのでHaskellを学ぶだけ.
(人間の)スペック
- TypeScriptまあまあわかる
- ADTが何かまあまあわかる
- Rustちょっとだけやった
- CopyとCloneしとけばなんとかなるっしょ?
- Elmちょっとだけやった
- 高1でやる程度のかんたんな集合とブール論理はわかる
- 論理学者 ハスケル・カリー (Haskell Curry) の名前に由来している.
-
https://en.wiktionary.org/wiki/Haskell によれば
- 古ノルド語の名 (下の名前) Áskellが父称になって生まれた姓
- エゼキエル (旧約聖書に登場するユダヤ人の預言者) に由来するユダヤ系の姓
- これらの姓から転用された名
- 父の名前サミュエルもどうやらユダヤ系っぽいのでたぶん家はユダヤ系なのだろう.勘違いかも
-
https://en.wiktionary.org/wiki/Haskell によれば
- 純粋関数型
- 遅延評価
- 主要な処理系はGHC (Glasgow Haskell Compiler)
- 名前の通りコンパイラ
- 遅延評価って言語の特徴ってことでいいんだろうか
- 処理系の特徴とかでは?
- 言語仕様に「処理系は遅延評価で作れよ」って書いてあったりするのかもしれない
- 関数型と手続き型とを対比させようとして出される関数型の例って宣言型なのでは?
- 宣言型がよくわからないので適当言ってる
- 宣言的というのがよくわかってない
- 定義の集合って感じ?
環境構築はなんとなくghcupを使う.選定理由はrustupと名前が似てるから.
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でソース開いた時にも訊かれるようなのでそのタイミングで入れてもよい).
Haskellの環境構築について参考になる記事
- Cabal vs Stack という対立構造がある
- npm vs Yarn っぽい
- Stack / Yarn は依存関係地獄の救世主として現れた
- Cabal / npm も負けじと機能改善を行ってどっちでもいい感じになった
VSCodeのHaskell拡張を入れる.インストール数が妙に少ないのは最近新しく出たからっぽい.
Cabalのプロジェクト構築する npm init -y 的なの
cabal init
ディレクトリ名.cabal って名前でYAMLっぽいけどなんか違う設定ファイルが生えた.依存関係とかライセンスとか書くようなので package.json みたいな感じそう
cabal init が自動的に Hello, world を生やし,手書きする愉しみを理不尽にも奪われる.
module Main where
main :: IO ()
main = putStrLn "Hello, Haskell!"
cabal run で実行される.cargo run じゃん.影響の順序は逆か?
別にCabalなんざ使わずとも ghc さえ入ってれば ghc Main.hs で実行ファイル作れるし runghc Main.hs で直接実行できる.とほほの入門ではこっち.入門の段階でパッケージ管理とか大仰なものが出てくると圧倒されるので,素朴な方法で体験できるのは重要だと思う.
Hello worldからも読み取れることがある.
- エントリポイントは
main -
mainはIO ()を返す- これは関数なのか?
- 関数呼び出しには () が不要
putStrLn が標準出力してるんではなく,putStrLn が出力を表す IO 値を返してそれが main の値になって外側で出力が行われるらしい.
-- 行コメント
{-
複数行コメント
-}
VSCodeでは Control-/ でその行をコメントアウト/コメント解除をトグルできる便利機能がある.-- を挿入するのに / を使わされるのは手続き型・オブジェクト指向というかC++/Java派生言語に囚われた人間の利己的な設計であり関数型への迫害である,的な言いがかりをつけられないでもない.
do で複数の式をまとめて実行できる.do {} としてブロックを書くこともできるらしいが,フォーマッタが優秀すぎて保存時に勝手に消されてインデントになる.
main = do
putStr "Hello, "
putStrLn "world!"
標準出力から1行取って出力するだけ.
main = do
s <- getLine
putStrLn s
s <- getLine は getLine (\s -> ...) のシンタックスシュガーで,要するに変数っぽいのは無名関数の引数名だ,みたいな話を読んだことがある.
見つけた
JavaScriptで、ある式の結果を変数に代入する
var input = getLine()というようなコードは変数を宣言する文です。Haskellでもinput <- getLineというような文を書くことがありますが、これは実はgetLine >>= (\input -> ... )というような式のシンタックスシュガーで、実はgetLineという式と(\input -> ...)という関数を演算子>>=に適用するという式です。<-という記号は演算子ではなく構文であり、inputという変数を宣言して代入しているというより、実はinputはただの無名関数リテラルの引数だったりします。
>>= だったわ
真偽値 Bool とか文字型 Char とかがある.Floatが32bit浮動小数点数,Doubleが64bit浮動小数点数.[Char] は文字のリストで String と等価.
Int と Integer の違い.Int の固定長整数 (最低30bit以上) ってすっごいフランクな仕様だな…
というか Int と Integer で違う型なのやさしくない.Rustの i32 usize f64 が一番.
(Int, Char) みたいなタプル型も作れる.関数型は Int -> Int とか Int -> Int -> Int とか.Elmで概念は知っているが複数引数関数はカリー化されている,と.それよりTypeScriptだと (n: number) => (m: number) => number みたいに引数名が要るのでうっかり書きそうになる.
トップレベルに変数を置く.関数型だと再代入できないので束縛って呼んだりするアレ.
x = 42
main = print 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
ところで
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個の型なので
サンプルコードこれ非合法じゃん.
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の分割代入っぽいけど分割代入がパターンマッチっぽいとの見解も出されており決着がつく見通しはない.
関数ですよどころか,演算子を () で囲うと関数になってしまう.
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 -> Bool の not 関数があったり不等価が /= だったりする.
. 関数合成演算子.なんか数学的には射?写像?の合成になるらしいが…
square x = x * x
twice x = x + x
twiceSquare = square . twice
squareTwice = twice . square
main =
do
print $ twiceSquare 10
print $ squareTwice 10
g . f は数学の関数合成
となり,Haskellコードでは
gf1 = g . f
gf2 x = g $ f x
と表現できる.要するに関数適用の順番は
関数定義でもパターンマッチができる.
f 0 = "Zero"
f 1 = "One"
オーバーロードっぽい.
ここで「オーバーロードは引数の型に対して実装が切り替わるシステムなんだから値に対する分岐を行っているだけのこれは違うだろ」というマサカリを振り上げて助走に入っているみな様がたはもちろんまったく正しいのだが,TypeScript にはリテラル型というのがあるので x: 0 と x: 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,あれ 0 と 1 にしか定義されてないくせに型推論では 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 a と Nothing という値のどちらか.失敗したら Nothing を返すというのがよくある使い方.
f 0 = Just "Zero"
f 1 = Just "One"
f n = Nothing
これで 50000 とか渡すのがいても問題ない.
パターンマッチと再帰を組み合わせることもできる.これがやばくて,たとえば階乗を求める関数は
fact 0 = 1
fact n = n * fact (n - 1)
と書ける.数式による定義は
である.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 式とかだとかなり自由に条件書けた気がしてこれに近いかも.
ここで出てくる otherwise は True が入っているだけである.
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"
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
if condition then positive else negative という式もある.else if はもしかしてない?
case でどこでもパターンマッチ.
f n = case n of
0 -> Just "Zero"
1 -> Just "One"
_ -> Nothing
これの 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 where で Type を型クラス 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’
Functor は fmap をもつ.最初 fmap は flatMap だと思ってたのだが違うようだ.functor map の略っぽくて,どうやら標準の map はリストにしか使えないらしい.ハァ~!?
ややこしいことにHaskellのFunctor は数学的には functor (関手) ではなく,functorの一種である endofunctor (自己関手) であるそう.「モナドは単なる 自己関手 の圏におけるモノイド対象だよ,なにか問題でも?」の自己関手.
要約すると
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
ここで fmap が f 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 f は fn <$> 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> ってことだしな.
ついこの間 (2020-01-15) 御年20歳を迎えられたWikipedia先生に詳しい講義をおねがいしようかな.
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 に相当する. return は Some と同じだからそれでいいじゃんって気もするけど…
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 には,「ox が None だったら 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が出力する.非純粋な部分を関数の外側に追いやるのを言語全体に適用しただけともいえる.
putStrLn は String -> IO () すなわちとくに何も得ることはない.getLine は IO 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 <- f は f >>= \x ->. .. の糖衣構文」というのは正しいが説明不足で,「f は f >> ...」である,という面があるというかなんというか.
状態モナドがわからんな…
State モナドに 「状態」 は入ってないです。
中身はただの 「関数」 です。
副作用なしで状態を遷移させちゃうよ モナドの魔法 で! とか勘違いですよー。
よく見たらピュアスクじゃねーか!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 で終わる.
タプルにして,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 のインスタンスであることを確かめたい.
newtype State s a = State (s -> (a, s))
PureScript,型クラスのインスタンス化まわりの文法と型クラスの種類とかが違いすぎてさすがにHaskellと読み替えながら進むのはつらい.つらいので新しい記事
を読んでいく.
「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).\_ \b が a 型,\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
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 a か a 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 だったら中身 a に f を適用した結果を Just に包んで返す」「Nothing だったら Nothing を返す」という働きをする.
Result e a についても同じことを考えると,Ok a が正常値で Err e が異常値として扱うなら fmap f の f は a -> 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’
f を Result 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) のときはどっちを返すべきなんだろうか?多重定義が定義順にマッチされるとしたらこの場合は関数側の値が取られそうだけど…
pure はもちろん Ok.
モナドに行く.>>= の型は
(>>=) :: 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 <- で取得されている a は Result e a の a 型に相当している.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