IPv4アドレスの文字列"192.168.1.1"をパースする方法を考える。IPAddrの内部表現は次のようにする。

1
type IPAddr = IPAddr Int Int Int Int

思いつくのは次の2通り。

  1. ピリオドでsplitして、整数値に変換する。
  2. パーサを利用する。

いずれにしても結構面倒。この記事では前者だけやる。

準備

適当なディレクトリで次のコマンドを実行。

$ elm init
$ elm install elm/parser

src/IPAddr.elmを作り、内容を以下の通りにする。

1
2
3
module IPAddr exposing (..)

type IPAddr = IPAddr Int Int Int Int
$ elm repl
> import IPAddr exposing (..)

方針

次の処理を行う関数をfromStringとして定義する。

  1. 文字列を.でsplitする。
  2. Listの要素数が4でなければ失敗。
  3. Listの各要素にString.toIntを適用し、どれか一つでも失敗すれば全体としても失敗。
  4. Listを[a,b,c,d]としたとき、IPAddr a b c dを返す。

traverseの実装

3の処理は、次の関数として抽象化できる: リストの各要素にfを適用し、その結果すべてがJustを返したときだけ、全体の結果を返す。

1
traverse : (a -> Maybe b) -> List a -> Maybe List b

原始的な実装

なるべくfoldrとかを使わずに書こうとするとこんな感じになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
traverse : (a -> Maybe b) -> List a -> Maybe (List b)
traverse f list =
  case list of
    [] ->
      Just []

    x::xs ->
      case traverse f xs of
        Nothing ->
          Nothing

        Just ys ->
          case f x of
            Nothing ->
              Nothing

            Just y ->
              Just (y::ys)

case文を使ってネストが深くなってくると、Haskellみたいなパターンマッチが欲しくなってくる。

Maybe.map2を利用した実装

上の実装で、Maybeを2回剥がして値を取り出し、それに対してリストの連結を行なっていることがわかる。実はこの処理を抽象化した関数Maybe.map2が実装されている。

どちらもJust値だったときのみ、その値を取り出して計算し、Justに包んで返す関数。

> import Maybe
> Maybe.map2 (\a b -> a + b) (Just 1) (Just 2)
Just 3 : Maybe.Maybe number
> Maybe.map2 (\a b -> a + b) Nothing (Just 2)
Nothing : Maybe.Maybe number
> Maybe.map2 (\a b -> a + b) (Just 1) Nothing
Nothing : Maybe.Maybe number
> Maybe.map2 (\a b -> a + b) Nothing Nothing
Nothing : Maybe.Maybe number

これを利用して書き直すと以下のようになる。

1
2
3
4
5
6
7
8
traverse : (a -> Maybe b) -> List a -> Maybe (List b)
traverse f list =
  case list of
    [] ->
      Just []

    x::xs ->
      Maybe.map2 (::) (f x) (traverse f xs)

これはなかなか感動的。

Maybe.map2 + foldrを利用した実装

上の定義を見ているとfoldrでもっと簡単に書けそうだと気づく。なので書く。

1
2
3
traverse : (a -> Maybe b) -> List a -> Maybe (List b)
traverse f list =
  List.foldr (\x acc -> Maybe.map2 (::) (f x) acc) (Just []) list

補足

これは実は車輪の再発明で、同じ関数がelm-community/Maybe.Extraで定義されている。

実はElmにもHoogleみたいに、型名から関数を検索するサービスがある。実際、「こんな型の関数ないかな」と思ってElm Searchで検索したら出てきた。

fromStringの実装

ようやく本題に入る。といってもtraverseさえできればあとは簡単。

まずは、文字列を整数値に変換し、さらに[0,255]に収まっているかどうかをチェックする関数parseByteを定義する。Maybe.andThenは以下のように、Maybe値に対してさらに条件をかけて、不正なものを落とすために使われる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
parseByte : String -> Maybe Int
parseByte string =
  String.toInt string
  |> Maybe.andThen toByteInt

toByteInt : Int -> Maybe Int
toByteInt n =
  if 0 <= n && n <= 255 then
    Just n
  else
    Nothing

これを元にfromStringを実装する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fromString : String -> Maybe IPAddr
fromString string =
  let list = string
             |> String.split "."
             |> traverse parseByte
  in
    case list of
      Just [a,b,c,d] ->
        Just (IPAddr a b c d)

      _ ->
        Nothing
> IPAddr.fromString "192.168.1.1"
Just (IPAddr 192 168 1 1)
    : Maybe IPAddr.IPAddr
> IPAddr.fromString "192.168.1"
Nothing : Maybe IPAddr.IPAddr
> IPAddr.fromString "192.168.1.1.1"
Nothing : Maybe IPAddr.IPAddr
> IPAddr.fromString "192.168.1.255"
Just (IPAddr 192 168 1 255)
    : Maybe IPAddr.IPAddr
> IPAddr.fromString "192.168.1.256"
Nothing : Maybe IPAddr.IPAddr