読者です 読者をやめる 読者になる 読者になる

きくらげ観察日記

好きなことを、適当に。

Elmの型関連が弱すぎてつらい

inkar-us-i.hatenablog.com

ここでやたらElm推してるみたいな書き方をしてしまいましたが、僕自身はそんなにElm好きではありません。

型クラスがない

Elmには型クラスが無いため、まともに多相的な関数を書くことはできません。
例えば、Elmには至る所でモナディックな処理が出てきます。

-- Maybeモナド
Just 3
  |> Maybe.andThen (\x -> if x == 3 then Nothing else Just (x + 1))
-- Cmdモナド
Http.get string "http://hoge.com"
  |> Task.andThen (\url -> Http.get (list string) url)
-- Random.Generatorモナド
Random.int 0 10
  |> Random.andThen (\n -> Random.int n 100)

この andThen がモナドの >>= に相当するのですが、これらの3つのandThenは全て別々に実装されたものとなります。
この他にも、haskellのliftM, liftM2 に対応するmap, map2もそれぞれのライブラリで定義されているのですが、これらは全て「別々に」定義されています。
実際は、「a -> m b」なる関数returnと、このandThenの2つの関数さえ用意すれば、残りのmap2等の関数は全てそこから自動的に定義できるはずです。

また、例えば同じ生成器で乱数をn回生成するrepeatGenという関数を作ろうと思ったとします。
この関数は以下のように書くことができます。

repeatGen : Int -> Generator a -> Generator (List a)
repeatGen n gen =
    let return x = Random.map (\_ -> x) Random.bool -- return が無いので代用。厳密にはモナド則を満たさなくなるけど今は無視
    in if n <= 0 then return []
       else Random.map2 (::) gen (repeatGen (n - 1) gen)

この関数はだいたい思った通りに動作します。
この後、同じタスクをn回実行するrepeatTaskが欲しくなったとします。

repeatTask : Int -> Generator a -> Generator (List a)
repeatTask n task =
    let return x = Task.succeed x
    in if n <= 0 then return []
    else Task.map2 (::) task (repeatTask (n - 1) task)

この定義を見てください。ほとんどrepaetGenと同じです。
Haskellの場合は、一般のモナドについて以下のようにrepeatMを定義することができます。

repeatM :: Monad m => Int -> m a -> m [a]
repeatM n action =
    if n <= 0 then return []
    else liftM2 (:) action (repeatM (n - 1) action)

しかし、Elmは言語機能が貧弱なため、このような関数をElmで書くことはできません。
不便ではありますが、言語仕様なので我慢するしかありません。

高階多相やRankN多相が使えない

Haskellの場合、型クラスという言語機能がなかったとしても、以下のようにMonadを明示的に渡してしまうことによって、少し不便ではありますが似たような操作を行うことができます。

data Monad m = Monad {
    return :: a -> m a
    -- Elm 風に
  , andThen :: (a -> m b) -> m a -> m b
  }

-- Elm 風に
(|>) :: a -> (a -> b) -> b
a |> f = f a
infixl 1 |>

map2 :: Monad m -> (a -> b -> c) -> m a -> m b -> m c
map2 monad f ma mb =
  ma |> andThen monad (\a -> mb |> andThen monad (\b -> return monad (f a b)))

repeatM' :: Monad m -> Int -> m a -> m [a]
repeatM' monad n action =
  if n <= 0 then return monad []
  else map2 monad (:) action (repeatM' monad (n - 1) action)

しかし、これをElmでやろうとした場合、forallに相当する機能が無いため、このような関数を書くことはできません。
また、そもそも高階多相が無いため、

return : a -> f a

のような関数を定義することすらできません。

Functorもない

「いや、OCamlだって高階多相やRankN多相は無いし、それだけでじゃディスる理由にならないだろ」と言う方もいるかもしれません。それはたしかにその通りです。しかし、OCamlにはその代わりにFunctorという素晴らしい機能があります。

module type MONAD =
  sig
    type 'a t
    val return : 'a -> 'a t
    val and_then : ('a -> 'b t) -> 'a t -> 'b t
  end

module Make (M : MONAD) =
  struct
    let map2 (f : 'a -> 'b -> 'c) (ma : 'a M.t) (mb : 'b M.t) : 'c M.t =
      ma |> M.and_then (fun a -> mb |> M.and_then (fun b -> M.return (f a b)))
    let rec repeat (n : int) (action : 'a M.t) : 'a list M.t =
      if n <= 0 then M.return []
      else map2 (fun x y -> x :: y) action (repeat (n - 1) action)
  end

Functorを使うことによって、使用方法は少し異なりますがモナドを書くことができます。
重要なのは、一度このFunctor Makeを書いてしまえば、Random.GeneratorだろうがTaskだろうが同じように使うことができることです。

しかし、ElmにはFunctorもありません。したがって、この方法でも多相的な関数を書くことはできません。

その代わりにある謎機能

しかし、組み込み関数の(+)はIntだろうがFloatだろうが使えますし、(++)はListだろうがStringだろうが連結できてしまいます。これらは一体どのように定義されているのでしょうか?

> 3.0 + 2.0
5 : Float
> [1, 2, 3] ++ [4, 5, 7]
[1,2,3,4,5,7] : List number
> "hoge" ++ "fuga"
"hogefuga" : String
> (+)
<function> : number -> number -> number
> (++)
<function> : appendable -> appendable -> appendable

それぞれnumber, appendableという型変数名になっています。
実はこれは処理系による例外的な機能で、変数名の頭が「number」「appendable」である型変数は特別扱いされ、numberなら数値、appendableならリストや文字列などの連結できる任意の型を受け取ることができるようになっています。この他に、比較可能な型を表すcomparableなどもあります。

しかし、これらはあくまで「例外」的な機能です。新しく作った型をnumberやappendableのメンバーとして登録することもできませんし、これらの「型クラスのようなもの」を新たに作ることもできません。


このようにElmは型に関する機能が十分ではないため、どんな場合にも使用できるような高度に抽象化された関数を書くことはできません。
出来合いのライブラリを使って何かするだけなら便利かもしれませんが、ライブラリを作る側に回った時には非常に不便な言語です。