Elmメモ - 文字列をIPアドレスに変換(2) Parserを用いる方法
準備
前回の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つのステップに分かれる。
- Parserを作る
- 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
パーサーを組み合わせるときの基本になる。
|.と|=
|.
演算子は、右辺のParserの結果を無視した新しいParserを返す。|=
演算子は、右辺のParserの結果を左辺のParserの値に適用した新しいParserを返す。
何を言っているのかわかりづらいと思うので例を示す。
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
出力結果は前回の記事と同じなので省略。