Elmでリストの中身がすべて同じ値か確認する方法
コード
まずはコードを。
> f list = Maybe.map2 (\hd tl -> List.all(\v -> v == hd) tl) (List.head list) (List.tail list)
<function> : List a -> Maybe Bool
実行すると以下のようになります。
> f []
Nothing : Maybe Bool
> f [1]
Just True : Maybe Bool
> f [1,1]
Just True : Maybe Bool
> f [1,2]
Just False : Maybe Bool
> f [1,1,1]
Just True : Maybe Bool
> f [1,1,2]
Just False : Maybe Bool
>
処理の内容としては、まずはリストを先頭要素とそれ以外の残りのリストに分割しています。
そして、残りのリストの中身がすべて先頭要素と一致しているならTrue
、一つでも一致していなければFalse
を返しています。
少し解説
Elmでは、List.head
とList.tail
はMaybe
型を返してきます。
> List.head [1,2,3]
Just 1 : Maybe number
> List.tail [1,2,3]
Just [2,3] : Maybe (List number)
>
Maybeで結果が包まれているので、実際に利用する際には当然値を取り出す必要があります。 コレはちょっと面倒くさいです。 しかし、例えばApache Groovyでは結果をOptionなどで包んで返してくれないので、もしリストの中身が空だと実行時エラーになります。
groovy:000> [].head()
ERROR java.util.NoSuchElementException:
Cannot access first() element from an empty List
groovy:000> [].tail()
ERROR java.util.NoSuchElementException:
Cannot access tail() for an empty iterable
groovy:000>
ElmはMaybeに包み込んでくれるので、同様のことをしても当然実行時エラーにはなりません。
> List.head []
Nothing : Maybe a
> List.tail []
Nothing : Maybe (List a)
>
そしてMaybeで包まれているとエラー時の扱いをコンパイラが強制してくれるので、値が無い場合の処理を書き忘れることもありません。ありがたい話です!
Maybeな値の取扱
実際にMaybeな値を利用するサンプルはcase
を使います。
Maybeで包まれた値を2倍する処理を考えます。
> case Just 100 of
| Nothing -> 0
| Just v -> v * 2
|
200 : number
>
これで型安全な処理が実現できました!
しかし、たかが値を2倍する処理のために態々ココまでコードを書くのは面倒です。
Nothing
な状態でゼロを返すことは本当に正しいことなのか?ということも考える必要があります。
そういった問題をMaybe.map
関数を使うことで簡単に解決できます。
> Just 100 |> Maybe.map(\n -> n * 2)
Just 200 : Maybe number
>
シンプルになりました!
実行結果を自動的に再度Maybeに包んでくれています。
さらに、Maybe.map
は渡された値がNothing
の場合はそのままNothing
を返してくれます。
> Nothing |> Maybe.map(\n -> n * 2)
Nothing : Maybe number
>
非常に楽ちんですね!! mapはさらにmapで繋ぐことが可能です。
> Just 100 |> Maybe.map(\n -> n * 2) |> Maybe.map(\n -> n - 10)
Just 190 : Maybe number
>
途中でNothingを返す場合
本題とはズレますが、Maybe.map
で処理を繋げることが出来るのは、最初に渡されたJustかNotingのまま変わらない処理になります。
サンプルとして、渡された値を+1 して、偶数ならそのままJustで包んで奇数ならNothing、そして更に値を半分にする処理を考えます。
Maybe.map
を使えば実現できそうです。
> Just 1 |> Maybe.map (\n -> n + 1) |> Maybe.map (\n -> if (modBy 2 n) == 0 then Just n else Nothing) |> Maybe.map (\n -> n // 2)
-- TYPE MISMATCH ---------------------------------------------------------- REPL
This function cannot handle the argument sent through the (|>) pipe:
11| Just 1 |> Maybe.map (\n -> n + 1) |> Maybe.map (\n -> if (modBy 2 n) == 0 then Just n else Nothing) |> Maybe.map (\n -> n // 2)
^^^^^^^^^^^^^^^^^^^^^^^
The argument is:
Maybe (Maybe Int)
But (|>) is piping it to a function that expects:
Maybe Int
Hint: Use Maybe.withDefault to handle possible errors. Longer term, it is
usually better to write out the full `case` though!
>
エラーになってしまいました。
エラー内容を読めば一目瞭然で、単純に型が合わない、という話です。
なぜ型が合わないかというのは、2つめのMaybe.map
に渡している関数が、手動でJust
かNothing
を返しているためです。
Maybe.map
は渡された関数を実行した後、自動的にその値をJust
に包んで返してくれます。
Nothing
の場合はそもそも関数を実行せずにNothing
を返すだけです。
そのため、今回は手動でJust
に値を包んで返してしまったので、最終的にさらにJust
に包まれて入れ子状態になってしまっています。
しかし奇数の場合はNothing
を返したいので、型シグネチャ的にはJust
を返すことが必須です。
そこで、Maybe.map
の代わりにMaybe.andThen
を利用します。
基本的な使い方は同じですが、Maybe.andThen
は渡した関数でreturnする値は手動でMaybe
に包んであげる必要があります。
> Just 1 |> Maybe.andThen (\n -> Just (n + 1)) |> Maybe.andThen (\n -> if (modBy 2 n) == 0 then Just n else Nothing) |> Maybe.andThen (\n -> Just (n // 2))
Just 1 : Maybe Int
> Just 2 |> Maybe.andThen (\n -> Just (n + 1)) |> Maybe.andThen (\n -> if (modBy 2 n) == 0 then Just n else Nothing) |> Maybe.andThen (\n -> Just (n // 2))
Nothing : Maybe Int
>
できました!
複数のMaybeを同時に扱う
さて、今回List.head
とList.tail
で取得した値を使ってすべての値が同じかどうかを確認する必要があります。
2つのMaybe
な値があります。これらはどう扱えば良いのでしょうか?
一番シンプルな方法(私が知る限り)はMaybe.map2
を使う方法です。
Maybe.map2
は第2、第3引数にMaybeな値を渡してあげます。
そして第1引数に渡す関数は、その2つの値を取る関数を渡してあげます。
> Maybe.map2 (\a b -> a + b) (Just 1) (Just 2)
Just 3 : Maybe number
>
ちなみにいずれかの値がNothing
の場合は、渡した関数は実行されずに単純にNothing
が返されます。
> Maybe.map2 (\a b -> a + b) (Just 1) (Nothing)
Nothing : Maybe number
> Maybe.map2 (\a b -> a + b) (Nothing) (Just 2)
Nothing : Maybe number
>
リスト内の値が同じかどうか
コレは特段難しいことはなく、List.all
で値を比較してあげるだけです。
> List.all (\n -> n == 1) [1,1]
True : Bool
> List.all (\n -> n == 1) [1,2]
False : Bool
>
まとめ
ココまで来ると最初のコードのすべてが分かります。
f list = Maybe.map2 (\hd tl -> List.all(\v -> v == hd) tl) (List.head list) (List.tail list)
Maybe.map2
なので、引数は3つになります。
第2引数はリストの最初の値、第3引数は最初以外の残りの値が詰まったリストです。
そして(\hd tl -> List.all(\v -> v == hd) tl)
では、リストの残りの値(tl
)の要素がすべて最初の値(hd
)と同じかどうかを確認しています。
すべて要素が同じなら Just True
が返ってきて、一つでも異なる値が有ればJust False
が返ります。
そもそも値が無い場合(空の配列)の場合はNothing
が返されます。
おわりに
今回の内容は関数型プログラミング言語に慣れている人で有れば特段難しい内容ではない、当たり前な内容だと思います。 10年ほど前に少しだけScalaを触って、最近OCamlを勉強していたので私自身なんとなく理解はできました。
しかし、理由はわかりませんがElmはこういった諸々の関数型プログラミングのエッセンスが自然と頭に入ってきやすい気がしています。 やはりGUI(HTML)を扱うという具体的で身近な目的の為に利用するのでそういったことが学習のモチベーションに繋がっているのかもしれません。
公開日:2020/06/09