Chanomic Blog

Elmメモ - 文字列をIPアドレスに変換(2) Parserを用いる方法

(last modified:
)

準備

前回のsrc/IPAddr.elmを全て消し、内容を以下の通りにする。

module IPAddr exposing (..)

import Parser

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

Parserの基本

以下の2つのステップに分かれる。

  1. Parserを作る
  2. Parserを実行する - Parser.runを用いる

ライブラリでは、標準で用意されているParserと、それらを組み合わせて新たなParserを作るための関数が用意されている。

> run int "123"
Ok 123 : Result (List Parser.DeadEnd) Int
> run int "123abc"
Ok 123 : Result (List Parser.DeadEnd) Int
> run int "abc123abc"
Err [{ col = 1, problem = ExpectingInt, row = 1 }]
    : Result (List Parser.DeadEnd) Int

succeed

何もパースせず、決まった結果だけを返すパーサー。

> run (succeed "Hello") "123abcde"
Ok "Hello" : Result (List Parser.DeadEnd) String

パーサーを組み合わせるときの基本になる。

|.と|=

何を言っているのかわかりづらいと思うので例を示す。

add : Int -> Int -> Int
add a b =
  a + b

parser : Parser Int
parser =
  succeed add
    |= int
    |. spaces
    |= int
> run parser "1 2"
Ok 3 : Result (List DeadEnd) Int

以下の型はParser (Int -> Int -> Int)である。

succeed add

左辺のParser (Int -> Int -> Int)の値に右辺のParser Intの値Intを適用すると、結果の型はParser (Int -> Int)となる。

succeed add
    |= int 

|. spacesによって、スペースはパースの結果に影響しない。結果の型はParser (Int -> Int)のまま。

succeed add
    |= int 
    |. spaces

左辺のParser (Int -> Int)の値に右辺のParser Intの値Intを適用すると、結果の型はParser Intとなる。

succeed add
    |= int 
    |. spaces
    |= int 

カスタム型やレコードも関数みたいなものなので、次のようにしてパース結果を各フィールドに入れることができる。

type TwoInt = TwoInt Int Int

parser : Parser TwoInt
parser =
  succeed TwoInt
    |= int
    |. spaces
    |= int
> run parser "1 2"
Ok (TwoInt 1 2)

fromString作成(失敗例)

ということで次のように定義すればIPアドレスがパースできそうである。

fromString : String -> Maybe IPAddr
fromString string =
  case run ipParser string of
    Ok addr ->
      Just addr

    Err _ ->
      Nothing


ipParser : Parser IPAddr
ipParser =
  succeed IPAddr
    |= int
    |. symbol "."
    |= int
    |. symbol "."
    |= int
    |. symbol "."
    |= int
    |. end

ところが、これはうまくいかない。

> fromString "192.168.1.1"
Nothing : Maybe IPAddr
> run ipParser "192.168.1.1"
Err [{ col = 1, problem = ExpectingInt, row = 1 }]
    : Result (List DeadEnd) IPAddr

これは、ピリオドのせいでIntではなくFloatと認識されてしまったことが原因。実際、ピリオドではなく他の文字で代用するとうまく動く。

Chompers

「elm parser period」でググったら、まったく同じ悩みを持っている人がいた。そこを参考にしつつ自分でコードを書く。

結局、標準搭載のintは使わずに、数字を一文字ずつ読み取っていく戦略をとる。そのために、elm/parserのChompersの関数群の力を借りる。

chompWhile

ある条件を満たしている間読み進めるだけのパーサを作成する。「数字が現れている間読み続ける」パーサは以下のように定義できる。

digit : Parser ()
digit =
  chompWhile Char.isDigit

getChompedString

chompWhileで読み進めた値をString値として取得するParserを作る。上の関数は次のように書き直せる。

digit : Parser String
digit =
  getChompedString
  <| chompWhile Char.isDigit

andThen

digitで得られた値はString型なので、これをInt型に変換した新しいParserが欲しい。ついでに、値が[0,255]に収まっているかどうかもチェックして、不正ならエラーを吐くようにしたい。これはParser.andThen関数で実現できる。パースによって得られた値に対して、条件をチェックしたり、値を変換したりするために利用される。単に値を変換するだけなら、Parser.mapを利用しても良い。

byte : Parser Int
byte =
  digit
  |> Parser.andThen parseByte

parseByte : String -> Parser Int
parseByte string =
  let x = String.toInt string
          |> Maybe.andThen checkByte
  in
    case x of
      Just n ->
        succeed n

      Nothing ->
        problem "Parse Error"


checkByte : Int -> Maybe Int
checkByte x =
  if 0 <= x && x <= 255 then
    Just x

  else
    Nothing

fromStringの実装

作ったbyteを使う。以下のようにする。

fromString : String -> Maybe IPAddr
fromString string =
  case run ipParser string of
    Ok addr ->
      Just addr

    Err _ ->
      Nothing


ipParser : Parser IPAddr
ipParser =
  succeed IPAddr
    |= byte
    |. symbol "."
    |= byte
    |. symbol "."
    |= byte
    |. symbol "."
    |= byte
    |. end

出力結果は前回の記事と同じなので省略。