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.headList.tailMaybe型を返してきます。

> 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に渡している関数が、手動でJustNothingを返しているためです。 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.headList.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

Elm

About me

ドイツの現地企業でWeb Developer/System Administratorとして働いているアラフォーおじさんです。

プログラミングとかコンピュータに関する事がメインですが、日常的なメモとか雑多なことも書きます。

Links :
目次

コード


少し解説


Maybeな値の取扱


途中でNothingを返す場合


複数のMaybeを同時に扱う


リスト内の値が同じかどうか


まとめ


おわりに