Idris面白機能:with構文と依存型、View

このエントリはIdris Advent Calendar 2020の18日目の記事です。

κeenです。Idrisで個人的に面白いなーと思ってる機能、 with 構文とそれを利用したViewについて紹介します。

パターンマッチを補助する with 構文

関数を書くときに条件分岐したくなることがありますよね。 データ型の構造に沿う条件であれば引数でのパターンマッチで済むんですが、もう少し複雑な条件だと ifcase を使わざるを得なくなります。 例えば「リストに値がなければ追加する」関数 addToList はこう書けるでしょう。

addToList : Eq a => a -> List a -> List a
addToList x xs =
  if elem x xs
  then xs
  else x::xs

こういうのも引数のパターンマッチで書けると綺麗ですよね。 これは with 構文で解決できます。 関数名 引数… with (補助式) 節… の構文です。 節は 関数名 引数… | 補助パターン = 本体 と書きます。

例えば先程の addToList だと以下の構文になります。

addToList : Eq a => a -> List a -> List a
addToList x xs with (elem x xs)
  addToList x xs | True  = xs
  addToList x xs | False = x :: xs

関数の本体が = xs= x :: xs でサッパリしたので気持良いですね。

ところで、上記の記述 addToList x xs が続いてちょっとくどいですよね。これは省略できます。 以下のようにも書けるのです。

addToList : Eq a => a -> List a -> List a
addToList x xs with (elem x xs)
  | True  = xs
  | False = x :: xs

もっとサッパリしましたね。

with の発展的利用例

with 構文の面白いところは、計算した結果をパターンマッチにかけられるところです。

例えばUNIXタイムスタンプが定義してあるとします。

data Timestamp = MkTimestamp Int

これを受け取って時刻の文字列を返す関数を定義してみましょう。 タイムスタンプから時刻を抜き出す関数と時刻から文字列を作る関数に分けると具合がよさそうです。

ということでまずはタイムスタンプから時刻を抜き出しましょう。JSTにあわせて+9時間しています。

toTime : Timestamp -> (Int, Int, Int)
toTime (MkTimestamp t) =
  let time = t `mod` (60 * 60 * 24) in
  let h =  time `div` (60 * 60) in
  let m = (time `div` 60) `mod` 60 in
  let s = time `mod` 60 in
  ((h + 9) `mod` 24, m, s)

これと with 構文を使えば時刻を表示する関数 showTime は至極完結に書けます。

showTime : Timestamp -> String
showTime t with (toTime t)
  | (h, m, s) = (show h) ++ ":" ++ (show m) ++ ":" ++(show s)

toTime により Timestamp の別表現が与えられたような形になっていますね。

依存パターンマッチ

Viewの話をする前に依存パターンマッチを紹介しましょう。 Idrisでは型パラメータなども {} で取り出せることは説明しましたね? 例えば Vect n an は以下のように取り出せます。

length : Vect n a -> Nat
length {n = n} _ = n

この n の部分でパターンマッチができるのですが、ちょっと面白い振る舞いをします。 以下の関数を見てみて下さい。

append : Vect n a -> Vect m a -> Vect (n + m) a
append {n=Z}   []        ys = ys
append {n=S k} (x :: xs) ys = append (x :: xs) ys

nVect で2つのパターンマッチが走っていますね。 n には ZSuc nVect には []x :: xs のパターンがあるので普通なら 2 x 2 = 4 種類のパターンがあるはずです。ところが、ここでは2通りのパターンマッチしかしていません。 nVect の長さに連動しているので n = Z のときには Vect[] ですし、 n = S k のときは Vectx :: xs です。それ以外の可能性はありません。 なので上記2通りで全ての場合を尽しているのです。

証拠に例えば n = S k かつ Vect[] のパターンを追加するとエラーになります。

append : Vect n a -> Vect m a -> Vect (n + m) a
append {n=Z}   []        ys = ys
append {n=S k} []        ys = append (x :: xs) ys
append {n=S k} (x :: xs) ys = append (x :: xs) ys

これをコンパイルすると以下のエラーが出ます。

- + Errors (1)
 `-- withAndView.idr line 48 col 0:
     When checking left hand side of append:
     When checking an application of Main.append:
             Type mismatch between
                     Vect 0 elem (Type of [])
             and
                     Vect (S k) a (Expected type)
             
             Specifically:
                     Type mismatch between
                             0
                     and
                             S k

このように依存型を使うと型と値が軛で繋がれたように連動するのです。 この仕組みを利用したのがViewです。

View

依存型を使うとパターンマッチのときに型と値が連動すると紹介しました。 では、とある値を受け取ったときにそれに依存する型を作れば値のパターンマッチを自由に制御できるのではないかという発想に至ります。実際可能で、それを利用するプログラミングパターンがViewです。 具体例をいくつか紹介しましょう。

Data.List.Views には List 型に対するViewがいくつかあります。 その中でも Split Viewを使ってみましょう。

Data type Data.List.Views.Split : List a -> Type
    View for splitting a list in half, non-recursively
    
    The function is: public export
Constructors:
    SplitNil : Split []
        
        
        The function is: public export
    SplitOne : Split [x]
        
        
        The function is: public export
    SplitPair : Split (x :: xs ++ y :: ys)
        
        
        The function is: public export

コンストラクタが3つあって、それぞれの型が Split []Split [x]Split (x :: xs ++ y :: ys) ですね。Split の値は split 関数で作れます。使ってみましょう。

splitView : List a -> List a
splitView l with (split l)
  splitView []                   | SplitNil  = []
  splitView [x]                  | SplitOne  = []
  splitView (x :: xs ++ y :: ys) | SplitPair = []

splitView の引数のパターンマッチ (| の左)が Split の値に応じて変化しているのが分かりますか? そして特に目につくのが3節目の (x :: xs ++ y :: ys) です。 ++ は関数なので普通はパターンマッチでは使えません。ところがリストに依存している Split の方がそういう型をしているのでこういうパターンも書けてしまうのです。

例えばこれを使ってマージソートが書けるでしょう。

mergeSort : Ord a => List a -> List a
mergeSort l with (split l)
  mergeSort []               | SplitNil = []
  mergeSort [x]              | SplitOne = [x]
  mergeSort (x::xs ++ y::ys) | SplitPair = merge (mergeSort (x::xs)) (mergeSort (y::ys))
  -- mergeはpreludeで定義されている

この機能は他の言語では中々見掛けないんじゃないでしょうか。

再帰View

Viewの中には再帰的に定義されるものがあります。例えば SnocList などです。 snoc というのは cons をさかさまにした文字列で、 cons がリストの先頭に要素を加えるのに対して snoc はリストの末尾に要素を加えます。なので SnocList はパターンマッチでリストの末尾からデータを取り出すときに使います。

今までの知識で書くとこうなりますよね?

reverseList : List a -> List a
reverseList l with (snocList l)
  reverseList []          | Empty    = []
  reverseList (xs ++ [x]) | Snoc _ = x :: (reverseList xs)

これでも動くんですが、効率が悪いです。 snocList はリストの末尾まで辿ります。 それを reverseList の呼び出しごとに呼んでいるので無駄です。

SnocListSnoc には実は xs の分の SnocList の値も保持されています。 _ で無視している部分ですね。これを使うと効率的に再帰ができそうです。 ですが、これをどうやって使いましょう?

1つの方法は補助関数を作ることです。

reverseList : List a -> List a
reverseList l = helper l (snocList l)
where
  helper : (l : List a) -> SnocList l -> List a
  helper []          Empty      = []
  helper (xs ++ [x]) (Snoc rec) = x :: (helper xs rec)

これでもいいんですが、ちょっと野暮ったいですよね? Idrisには with で使うViewを外から差し込む構文があります。 関数 引数 | view という構文です。

reverseList : List a -> List a
reverseList l with (snocList l)
  reverseList []          | Empty    = []
  reverseList (xs ++ [x]) | Snoc rec = x :: (reverseList xs | rec)

reverseList xs | rec の部分がそれです。ちょっと奇妙ですが便利ですね。

因みに内部的には with| を使った関数は helper のような定義に展開されているらしいです。

多重 with

with 使ってる途中で追加の with をはじめることもできます。

isSuffix : Eq a => List a -> List a -> Bool
isSuffix l1 l2 with (snocList l1)
  isSuffix []          _  | Empty = True
  isSuffix (xs ++ [x]) l2 | Snoc rec with (snocList l2)
    isSuffix _           []          |  _      | Empty   = False
    isSuffix (xs ++ [x]) (ys ++ [y]) | Snoc r1 | Snoc r2 =
      if x == y
      then isSuffix xs ys | r1 | r2
      else False

使いすぎると訳がわからなくなりそうですが、覚えておいて損はないでしょう。

まとめ

with 構文と依存型によるパターンマッチの面白い挙動、それを使ったViewについて紹介しました。 with 構文のもう1つの使い方として証明があるのですが、それはまた後日ということで。

Written by κeen