Idrisのインタフェースとモナドなどなど

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

κeenです。Idrisのインタフェースやモナドについて紹介します。

インタフェース

色々な言語にあるやつとだいたい一緒です。 厳密にいうとアドホックポリモーフィズムのための機構なのでHaskellの型クラスやRustのトレイトに例えた方がいいのですが、細かい話は置いておきましょう。

interface インタフェース名 型変数 where 本体 の構文で書きます。 本体の部分には値の型や実装などを書きます。値とは関数も含みます。 例えば任意の型の値を文字列にするインタフェース Show の定義は以下のように書けます。

例:インタフェース Show の定義

interface Show a where
    show : a -> String

aShow を実装する型を表します。 そして show には実装がありません。型だけ示しているのでインタフェースっぽいですね。 因みに、 Show はプレリュードに定義されているので自分で書かなくても使えます。

これを実装するには インタフェース名 型名 where 本体 の構文を使います。

例: Name 型にインタフェース Show を実装するコード

record Name where
  constructor MkName
  firstName, lastName: String

Show Name where
  show (MkName firstName lastName) = firstName ++ " " ++ lastName

実装の方には逆に型の宣言がありません。

実装した Show は普通の関数のように呼び出すだけで使えます。

Idris> show (MkName "Edwin" "Brady")
"Edwin Brady" : String

デフォルト実装

先ほどインタフェースの本体には関数の型や 実装 を書くと説明しました。 インタフェースに実装を持つこともできるんですね。

例えば等価比較を行なうためのインタフェース Eq はデフォルト実装を持ちます。

例:インタフェース Eq の定義

interface Eq a where
    (==) : a -> a -> Bool
    (/=) : a -> a -> Bool

    x /= y = not (x == y)
    x == y = not (x /= y)

等しい( == )は等しくない( /= )の逆だしその逆もまた然りという定義ですね。 どちらか一方だけ実装すればもう一方は自動でついてくる仕組みです。

関連型

Idrisでは特に特別なものではないんですが、HaskellやRustで関連型(associated type)と呼ばれているものも書けます。

例えばとある別の型からその型の値を取得できる Extract というインタフェースを考えてみましょう。それは以下のように定義できます。

例:インタフェース Extract の定義

interface Extract a where
  From: Type
  extract: From -> a

この From が関連型です。 Idrisでは型も値なので関数や値のように普通にメンバーに書けばそれで済みます。

上記の型を Name に実装してみましょう。 まず準備として Name を保持する型 Person を定義しておきます。

例:Person 型の定義

record Person where
  constructor MkPerson
  age: Int
  name: Name

すると Person から Nameextract できるので、以下のように Extractname に実装できます。

例:ExtractName に実装するコード

Extract Name where
  From = Person
  extract = name

多パラメータのインタフェース

インタフェースの型パラメータは複数書くことができます。 例えばある型から別の型に変換するインタフェース Cast は以下のように定義されています。

例:プレリュードでのインタフェース Cast の定義

interface Cast from to where
    cast : (orig : from) -> to

fromto の2つのパラメータがありますね。 実装するときも2つのパラメータを指定します。

例: CastDoubleInt に定義するときの書き出し

Cast Double Int where
  -- ...

Cast のパラメータに DoubleInt を指定しています。 cast の実装はプリミティブの呼び出しになるので省略しました。

インタフェースを実装できる条件

インタフェースの実装は1つの型につき1つしか持てません 1。また関数はインタフェースを実装できません。

例えば Name に対してもう1つの Show のインタンスを追加しようとするとコンパイルエラーになります。

例:2つ目の Show インタフェースを Name に実装しようとした際に出るエラー

- + Errors (1)
 `-- (no file) line 0 col -1:
     interface.idr:21:1-9:Main.Name implementation of Prelude.Show.Show already defined

逆に、意外と実装できるケースにプリミティブを含む既存の型にインタフェースを実装できるというものが挙げられます。

以下にその例を示します。

例:インタフェース Zero を定義し、それをプリミティブ Int に実装するコード

interface Zero a where
  zero : a

Zero Int where
  zero = 0

プリミティブである Int に対してインタフェースを実装できました。

ジェネリクスとインタフェース

ジェネリクスで扱う型に特定のインタフェースを実装していることを要求したい場合があります。

ジェネリクス関数へのインタフェース制約

例えば引数を2つ受け取って、その小さい方、大きい方の順で並べて返す関数を定義したいとします。そのときに < で比較する必要がありますよね。 今までの知識で関数を定義すると < が実装されていないのでコンパイルエラーになります

例: a 同士を比較できないためエラーになるコード例

ordered: a -> a -> (a, a)
ordered a b =
  if a < b
  then (a, b)
  else (b, a)
- + Errors (1)
 `-- interface.idr line 35 col 2:
     When checking right hand side of ordered with expected type
             (a, a)
     
     When checking argument b to function Prelude.Bool.ifThenElse:
             Can't find implementation for Ord a

そういうときは特定のインタフェースを実装している型のみ受け付ける制約を書きます。インタフェース名 変数名 => 型 の構文です。< 演算子は Ord インタフェースで定義されているため、上記の ordered を修正すると以下のようになります。

ordered: Ord a => a -> a -> (a, a)
ordered a b =
  if a < b
  then (a, b)
  else (b, a)

また、複数の制約を書きたい場合は (インタフェース名 変数名, インタフェース名 変数名, ....) => 型 と丸括弧で括ってカンマで区切って書きます。

例:ジェネリクスの型変数に複数のインタフェース制約を書いたコード

orderedMsg: (Ord a, Show a) => a -> a -> String
orderedMsg a b =
  let (a, b) = ordered a b in
  (show a) ++ " < " ++ (show b)

インタフェース実装へのインタフェース制約

インタフェース自身にも関数を含みますからインタフェースの実装にインタフェース制約を加えたいというのも自然な要求です。実際、そういう機能があります。 インタフェース制約 => インタフェース名 型名 where 本体 の構文です。 以下に例を示します。

例:プレリュードのタプルへの Eq の実装例

(Eq a, Eq b) => Eq (a, b) where
  (==) (a, c) (b, d) = (a == b) && (c == d)

余談ですが、Idrisの3つ組以上のタプルは2つ組の組み合わせの糖衣構文となっています。例えば (A, B, C)(A, (B, C)) です。 なので上記の2つ組のタプルの実装で全てのタプルの実装をカバーできるのです。

インタフェースの拡張

インタフェース制約をインタフェースの宣言に書くこともできます。 これは事実上既存のインタフェースを拡張した新しいインタフェースを定義していると捉えることもできますね。 interface インタフェース制約 => インタフェース名 型変数 where 本体 の構文になります。

例えばプレリュードの NegNum を拡張したインタフェースです。

例:プレリュードの NumNeg のコード

||| The Num interface defines basic numerical arithmetic.
interface Num ty where
    (+) : ty -> ty -> ty
    (*) : ty -> ty -> ty
    ||| Conversion from Integer.
    fromInteger : Integer -> ty

||| The `Neg` interface defines operations on numbers which can be negative.
interface Num ty => Neg ty where
    ||| The underlying of unary minus. `-5` desugars to `negate (fromInteger 5)`.
    negate : ty -> ty
    (-) : ty -> ty -> ty

高カインド多相とファンクタ

少し難しめの機能に入ります。 特に複雑という訳ではないんですが、抽象度が高いので慣れてないと理解に時間のかかる機能です。

List aMaybe a のようにジェネリクスなデータ型がありますね? これに対してインタフェースを定義したいとします。 例えば map なんかは分かりやすいでしょう。

例: List aMaybe a に対する素朴な map の実装

-- List
map : (a -> b) -> List a -> List b
map f []      = []
map f (x::xs) = f x :: map f xs

-- Maybe
map : (a -> b) -> Maybe a -> Maybe b
map f (Just x) = Just (f x)
map f Nothing  = Nothing

これを抽象化するインタフェースを定義します。 するとパラメータになるのは ListMaybe の部分です。 これらは型コンストラクタ、Idris的にいうと Type -> Type の値です。

Idris> :t List
List : Type -> Type
Idris> :t Maybe
Maybe : Type -> Type

従来の型変数(Idris的にいうと Type の値)とは異なるのでインタフェースの定義に少し手を加えます。具体的には型変数が Type ではなく Type -> Type であることを表わすために型注釈を加えます。構文は interface インタフェース名 (変数名: Type -> Type) where 本体 です。

例: プレリュードのインタフェース Functor の定義

interface Functor (f : Type -> Type) where
    map : (func : a -> b) -> f a -> f b

Functor (関手)は ListMaybe などのように「map できる型」を抽象化する型です。f の部分に ListMaybe などが当て嵌ります。

Functor インタフェースのおかげでこのように ListMaybe の値に対して1を足す関数を適用できます。

Idris> map (1+) (Just 1)
Just 2 : Maybe Integer
Idris> map (1+) [1, 2, 3]
[2, 3, 4] : List Integer

このように Type ではなく Type -> Type などの複雑な「型の型」を持つものに対するジェネリクスを高カインド多相と呼びます(「型の型」はカインド(kind)と呼ばれています)。

因みに map の代わりに <$> という演算子も使えます。

infixr 4 <$>
(<$>) : Functor f => (func : a -> b) -> f a -> f b
func <$> x = map func x
Idris> (1+) <$> (Just 1)
Just 2 : Maybe Integer
Idris> (1+) <$> [1, 2, 3]
[2, 3, 4] : List Integer

後述するApplicativeと組み合わせるときに便利です。

余談ですがHaskellだとリスト専用の mapFunctor で定義される fmap で分かれています。恐らくですが先にリストの map を作ったあとに Functor という抽象化に気付いたので要らぬ複雑性が入ってるんじゃないかと思います。その点IdrisはHaskellの後発なのもあってシンプルですね。

多引数関数とApplicative

map は便利ですが、痒いところに手が届かないことがあります。 多引数関数には使いづらいのです。

例えば2引数関数 (+)Just 1Just 2 に適用したいとしましょう。 そこで素朴に map で適用しようとするとエラーになります。

例: map を使って素朴に (+)Just 1Just 2 に適用した式

Idris> map (+) (Just 1) (Just 2)
(input):1:1-25:When checking an application of function Prelude.Functor.map:
        Type mismatch between
                Maybe a1 (Type of Just x)
        and
                (\uv => _ -> uv) a (Expected type)

        Specifically:
                Type mismatch between
                        Maybe
                and
                        \uv => _ -> uv

これは落ち着いて型を考えるとエラーになる理由が分かります。 map の型は (a -> b) -> Maybe a -> Maybe b です。 (+) の型は Integer -> Integer -> Integer で、これは Integer -> (Integer -> Integer) です。 これらを組み合わせると、 map (+)Maybe Integer -> Maybe (Integer -> Integer) になります。これを Just 1 に適用すると Maybe (Integer -> Integer) になります。 ここで関数ではなくて Maybe 型の値が出てきてしまいました。これでは Just 2 に適用できません。

しかしながらみなさんは無理矢理適用させる実装を思い付くんじゃないでしょうか。 以下のようにパターンマッチで取り出してしまえばいいのです。

例: Maybe に包まれた関数を無理矢理適用してしまうコード

ap: Maybe (a -> b) -> Maybe a -> Maybe b
ap (Just f) (Just x) = Just (f x)
ap _        _        = Nothing

実現できそうなのでインタフェースで抽象化しましょう。 Functor を拡張したインタフェースにするのが具合がよさそうです。 これはプレリュードで Applicative と呼ばれています。

例:プレリュードの Applicative の定義

infixl 3 <*>
interface Functor f => Applicative (f : Type -> Type) where
    pure  : a -> f a
    (<*>) : f (a -> b) -> f a -> f b

ap ではなく <*> という演算子になっていますが、やってることは先程定義した ap と同じものです。 これに対する Maybe の実装は以下のようになっています。

例:プレリュードの ApplicativeMaybe への実装

Applicative Maybe where
    pure = Just

    (Just f) <*> (Just a) = Just (f a)
    _        <*> _        = Nothing

ちゃんと <*> の実装が ap と同じものになっていますね。

map<$> と書けることと組み合わせて、以下のように使えます。

例: FunctorApplicative の利用

Idris> (+) <$> (Just 1) <*> (Just 2)
Just 3 : Maybe Integer

因みにインタフェースであるからには複数の型(型コンストラクタ)に実装されている訳です。例えば List での実装がどうなっているかというと、全ての要素に対して繰り返すようになっています。

例: List での FunctorApplicative の利用

Idris> (+) <$> [1, 2, 3] <*> [10, 11, 12]
[11, 12, 13, 12, 13, 14, 13, 14, 15] : List Integer

ところで Applicativepure というのがいますね。 これの役割に触れておきましょう。 funcx に適用するとします。 funcx の型がそれぞれ Maybe (一般化して f)に包まれている/いないで4つの組み合わせがありますね?それぞれどう適用するか見てみましょう。

関数 引数 適用
a -> b a func x
a -> b f a map func x
f (a -> b) a ????
f (a -> b) f a func <*> x

f (a -> b)a に適用する場合だけまだ出てきてませんね。 この隙間を pure が埋めてくれます。 func <*> (pure x) と書けばいいのです。

余談ですがFunctor を使った map func x(pure func) <*> xApplicative の機能だけで書くことができますね。そういった意味で ApplicativeFunctor の拡張になっています。

プログラムとモナド

ApplicativeMaybe などのジェネリクス型に包まれているデータや関数に対して操作できるようになりました。では新しく包む操作についてはどうでしょう。

例えば割る数が0以外では割った商を、0では Nothing を返す safeDiv を考えます。

例:safeDiv 関数

safeDiv : Integer -> Integer -> Maybe Integer
safeDiv _ 0 = Nothing
safeDiv d m = d `div` m

これを <$><*> で組み合わせることもできますが、結果はあまり嬉しくありません。

例:safeDivFunctorApplicative と一緒に使った例

Idris> safeDiv <$> (Just 1) <*> (Just 0)
Just Nothing : Maybe (Maybe Integer)

返り型が Maybe (Maybe Integer)Maybe が二重に出てきてしまいました。そして返り値も Just Nothing になってしまっています。これは数値が返ってきていないという意味では Nothing と変わりません。Maybe Integer に「潰せ」たらうれしいですよね。

そういう操作は一般に、flatten、あるいはjoinと呼ばれますね。 さらにflattenの派生型であると嬉しいのがflatMapです。Idrisの型で書くと Maybe a -> (a -> Maybe b) -> Maybe b です。flattenとflatMapは片方があればもう片方を定義できるので双子のような存在です。

そんなjoinとflatMapをインタフェースにしたのがモナドです。

例:プレリュードでの Monad の定義

infixl 1 >>=

interface Applicative m => Monad (m : Type -> Type) where
    ||| Also called `bind`.
    (>>=)  : m a -> ((result : a) -> m b) -> m b

    ||| Also called `flatten` or mu
    join : m (m a) -> m a

    -- default implementations
    (>>=) x f = join (f <$> x)
    join x = x >>= id

この Monad を使えば先程の safeDivMaybe Integer を返すように使えるようになります。

Idris> join (safeDiv <$> (Just 1) <*> (Just 0))
Nothing : Maybe Integer
Idris> Just 1 >>= \d => (Just 0 >>= \m => safeDiv d m)
Nothing : Maybe Integer

do 記法

先程の例、 Just 1 >>= \d => (Just 0 >>= \m => safeDiv d m) は見づらいですよね。演算子や無名関数が乱舞してどこに何が書いてあるのか分かりません。そこでこれを書きやすくする do 記法というのがあります。

先程のコードを do 記法で書き直すと以下のようになります。

do
  d <- Just 1
  m <- Just 0
  safeDiv d m

これだとぐっと見やすくなりますね。

ところで基本文法のところで触れ忘れたんですが、オフサイドルールには別の記法もあります。{記述1; 記述2; ...}{} で包んでそれぞれの記述を ; で分けます。こうすることで1行でも書けるようになります。 REPLなどでは1行で書きたいケースもあると思うのでお試し下さい。

例: do 記法をREPLで使うコード

Idris> do {d <- Just 1; m <- Just 0; safeDiv d m }
Nothing : Maybe Integer

モナドを使うときは大抵 do 記法を使うことになるでしょう。

Monad もまたインタフェースなので Maybe 以外の型も実装を持ちます。例えば List はその要素について繰り返します。 リスト内包表記のようなことを do 記法でもできるのです。

例:九九の左斜め下半分を do 記法で計算するコード

do
  x <- [0..9]
  y <- [0..x]
  pure (x * y)

モナドはDSL?

FunctorApplicativeMonad で何かに包まれた値を計算できるようになりました。では、包まれた値から取り出すにはどうしたらいいでしょう。 残念ながらいい方法はありません。 Nothing なんかは値がないから Nothing な訳で、そこから値を取り出せません。

逆に言うとモナドにすることで操作を「閉じ込めて」しまうことができます。 使える操作は FunctorApplicative で「持ち上げた」操作と、 Monad で「結合」できる a -> m b の型の関数のみです。

そういった意味でモナドはDSLと捉えることができます。 ライブラリなんかでもモナドを提供し、主な操作は do 記法でやるものが多くあります。

IOモナド

いままで、まともにHollo Worldを解説してませんでしたね。 それはIO操作もモナドで書かれているからです。

ということでモナドを知った今、改めてHello Worldをしてみましょう。 putStrLn は以下のような型をしています。

Idris> :t putStrLn
putStrLn : String -> IO ()

そしてIdrisは main : IO () な値からプログラムの実行を始めます。 なのでHello Worldは以下のように書きます。

例:IdrisでのHello World

main : IO ()
main = putStrLn "Hello, World"

これを Hello.idr として保存し、以下のように実行します。

例:Hello Worldをコンパイル・実行するコマンド

$ idris -o Hello Hello.idr
$ ./Hello
Hello, World

-o オプションをつけて idris コマンドを起動するとREPLではなくコンパイラが起動し、 -o で指定したファイルへとコンパイル結果を出力します。

もうちょっと複雑なことをしましょう。 getLine: IO String で標準入力から1行取得できます。 これと putStrLn で入力をエコーバックするプログラムはこう書けます。

例: getLineputStrLn を使ってユーザの入力を表示するプログラム

main : IO ()
main = getLine >>= \s => putStrLn ("Your input is " ++ s)

あるいは、 do 記法で書くこともできます。

例: getLineputStrLn を使ってユーザの入力を表示するプログラムを do 記法で書いたもの

main : IO ()
main = do
  s <- getLine
  putStrLn ("Your input is " ++ s)

これを Echo.idr に保存し、コンパイル、実行すると以下のように動作します。

$ idris -o Echo Echo.idr
$ ./Echo
echooooo
Your input is echooooo

ところでIOって何?

IO の型にちょっと違和感を覚えた方もいるんじゃないかと思います。 main の型は IO () という値です。関数じゃありません。 同じく getLineIO String という値です。 これだと書いたそばから実行されてしまわないでしょうか。 まあ、動いてるからにはそうならないんのは分かるんですが、どういう仕組みなんでしょう。

Idrisのプログラムからは IO の値を実行することができません。 getLine と書いたからといって即座に標準入力から文字列を取り出したりしないのです。 唯一 main に書いた IO の値のみが処理系側で実行されます。 処理系側で実行されてはじめて標準入力から文字列を取り出すというアクションが行なわれます。 IO は実行される前のプログラムのようなものなのです。

IO を実行できるのは mainの1箇所のみとなると、複数のIO処理をしたいときは IO の値を合成する必要があります。 その仕組みに選ばれたのがモナドという訳です。 >>= は別名 bind (結合)ですが、先程の getLineputStrLn のように複数のIO処理を結合するのに使われているのです。

純粋関数型言語とIO

さて、 main でしか IO を実行できないとなると他の関数内でIO処理をしたい場合はどうすればいいのでしょう。

1つの答えは「そういう関数は設計が悪いから書くな」です。 純粋関数型言語であるIdrisの基本方針として、IOや変数への破壊的代入などの計算以外の処理はよくないものとされています。 関数を呼んだときに何が起こるか分からなくなるからです。 なので関数内でIO処理を書きたくなったときはまずは「計算部分とIO部分に分離できないか」と考えてみましょう。

もう1つの答えは 「全て IO モナドの中で書く」です。 IO モナドをリレーのように main まで伝えればIOを実行できます。 なので関数の中でIOをしたければ IO モナドの中で書くことにすれば実現できます。 とはいえやっぱりIOの中でプログラムを書くのは面倒なので基本的には純粋な計算部分とIO部分に分けて、 IO部分でだけ IO モナドを使うようになります。

じゃあデバッグプリントを関数の中に仕込みたかったらどうなるの、という疑問はあるかもしれません。 まあ、普通に IO を使ってそれを呼ぶ関数を全て IO モナドの中で書くように変更します。 ちょっと面倒ですよね。一応そういった用途のためのバックドアの機構はあるのでそのうち紹介します。

まとめ

Idrisの重要な機能インタフェースと、重要なインタフェース Monad 、重要なモナド IO を立て続けに紹介しました。 IO まで辿りついたことでIdrisのプログラムを書けるようになりました。

未紹介のIdrisの機能もあるのでAdvent Calendarの残りでは手を動かしつつ他の機能も紹介していけたらなと思います。


  1. 名前付き実装という機能を使えばその限りではないのですが、話がややこしくなるので一旦置いておきます。 ↩︎

Written by κeen
Later article
Idrisと高橋君
Older article
REPLでIdris小旅行