REPLでIdris小旅行
このエントリはIdris Advent Calendar 2020の8日目の記事です。
κeenです。昨日の予告通り今回は手を動かすパートとしてIdrisのREPLを紹介することにします。
idris
を引数なしで起動するとインタラクティブシェルに入ります。
$ idris
____ __ _
/ _/___/ /____(_)
/ // __ / ___/ / ___/ Version 1.3.3
_/ // /_/ / / / (__ ) https://www.idris-lang.org/
/___/\__,_/_/ /_/____/ Type :? for help
Idris is free software with ABSOLUTELY NO WARRANTY.
For details type :warranty.
Idris>
ここに式を入力するとその評価結果を表示してくれます。
Idris> 1 + 1
2 : Integer
Idris>
これは入力の読み取り(Read)、評価(Eval)、表示(Print)を繰り返して(Loop)くれるのでREPLと呼ばれます。
言語の初学者にはいちいちファイルを書いてコンパイルして実行しなくても挙動を確認できるので便利ですね。 さらにIdrisはかなりREPLを作り込んでるので玄人にも重要な機能です。
それではこれを使っていきましょう。
式の評価
上にも書いたとおり、式を入力するとそれを計算して表示してくれます。
Idris> [1..10]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] : List Integer
Idris> [ i | i <- [1..10], i `mod ` 2 == 0]
[2, 4, 6, 8, 10] : List Integer
Idrisでは型も値として扱えることを思い出すと、型名を入力するとそれも評価して表示してくれることが分かります。
Idris> List
List : Type -> Type
Idris> Integer
Integer : Type
Idris> List
List : Type -> Type
idris> List Integer
List Integer : Type
とはいえ型は実体がないので特に計算とかは走りません。
一応計算を含む型であればそれを計算した結果が表示されます。
Idris> if True then Integer else String
Integer : Type
ところで「型の型」である Type
の型、 「型の型の型」が気になりませんか?
これを打ち込むとこう表示されます。
Idris> Type
Type : Type
Type
自身が Type
のインスタンスになってますね。
これではかの有名なラッセルのパラドックスになってしまいます。
しかし(私も自信はないですが)恐らくこれは表示を省略しているだけでそういう矛盾はIdrisには含まれていません。
REPLコマンド
さて、ここからが本番です。
IdrisのREPLには豊富なコマンドがあります。
その全貌はREPLに :?
または :help
と打つと表示されます。
Idris> :?
Idris version 1.3.3
-------------------
Command Arguments Purpose
<expr> Evaluate an expression
:t :type <expr> Check the type of an expression
:core <expr> View the core language representation of a term
:miss :missing <name> Show missing clauses
:doc <name> Show internal documentation
:mkdoc <namespace> Generate IdrisDoc for namespace(s) and dependencies
:apropos [<package list>] <name> Search names, types, and documentation
:s :search [<package list>] <expr> Search for values by type
:wc :whocalls <name> List the callers of some name
:cw :callswho <name> List the callees of some name
:browse <namespace> List the contents of some namespace
:total <name> Check the totality of a name
:r :reload Reload current file
:w :watch Watch the current file for changes
:l :load <filename> Load a new file
:! <command> Run a shell command
:cd <filename> Change working directory
:module <module> Import an extra module
:e :edit Edit current file using $EDITOR or $VISUAL
:m :metavars Show remaining proof obligations (metavariables or holes)
:p :prove <hole> Prove a metavariable
:elab <hole> Build a metavariable using the elaboration shell
:a :addproof <name> Add proof to source file
:rmproof <name> Remove proof from proof stack
:showproof <name> Show proof
:proofs Show available proofs
:x <expr> Execute IO actions resulting from an expression using the interpreter
:c :compile <filename> Compile to an executable [codegen] <filename>
:exec :execute [<expr>] Compile to an executable and run
:dynamic <filename> Dynamically load a C library (similar to %dynamic)
:dynamic List dynamically loaded C libraries
:? :h :help Display this help text
:set <option> Set an option (errorcontext, showimplicits, originalerrors, autosolve, nobanner, warnreach, evaltypes, desugarnats)
:unset <option> Unset an option
:color :colour <option> Turn REPL colours on or off; set a specific colour
:consolewidth auto|infinite|<number>Set the width of the console
:printerdepth [<number>] Set the maximum pretty-printer depth (no arg for infinite)
:q :quit Exit the Idris system
:version Display the Idris version
:warranty Displays warranty information
:let (<top-level declaration>)...Evaluate a declaration, such as a function definition, instance implementation, or fixity declaration
:unlet :undefine(<name>)... Remove the listed repl definitions, or all repl definitions if no names given
:printdef <name> Show the definition of a function
:pp :pprint <option> <number> <name>Pretty prints an Idris function in either LaTeX or HTML and for a specified width.
:verbosity <number> Set verbosity level
まだ紹介してない機能向けのコマンドなどもあるのでこれを見ただけだと混乱するかもしれません。 ゆっくり1つずつ試していきましょう。
関数を定義する
REPLに打ち込んで評価できるのは式までで、 foo = 1
のような変数定義は扱えません。
その代わり :let
コマンドで定義できます。
試してみましょう。
Idris> :let foo : Integer
Idris> foo
foo : Integer
Idris> :let foo = 1
Idris> foo
1 : Integer
定義できていますね。
関数も定義できますが、少し癖があるようです。
Idris> :let add : Integer -> Integer -> Integer
Idris> :let add x y = x + y
When checking an application of function Prelude.Interfaces.+:
No such variable x
Idris> :let add = \x, y => x + y
Idris> :t add
add : Integer -> Integer -> Integer
関数は後述するファイルを読み込む方法を使った方がよさそうです。
定義した関数は :unlet
(:undefine
)で未定義に戻すことができます。
Idris> :undefine
Undefined foo,foo,add,add.
型の表示
:t
あるいは :type
コマンドで式の型を表示できます。
Idris> :t 1
1 : Integer
Idris> :t Integer
Integer : Type
Idris> :t putStrLn
putStrLn : String -> IO ()
式の評価とあまり変わらなそうな気がしますが、評価に時間のかかる式で型情報だけほしい場合に便利です。
ここで例の Type
の型を見てみましょう。
Idris> :t Type
Type : Type 1
Type 1
と表示されました。
じゃあその型は…となると察しのとおり Type 2
になるはずですが、今のところユーザが手で書く手段がないので実験的に確かめることはできません。
Idris> :t (Type 1)
builtin:Type mismatch between
Type (Type of Type)
and
_ -> _ (Is Type applied to too many arguments?)
詳しくは省きますがIdrisの型システムはラッセルのパラドックスが起きなように設計されていて、型の型、型の型の型、型の型の型の型…はそれぞれ異なるようになっています。その階層を表わすのが Type 1
についている1という数字です。
ファイルを扱う
REPLにファイルを読み込んでみましょう。
recodr.idr
というファイルに以下の内容を書いて保存します。
record Person where
constructor MkPerson
age: Int
name: String
ロード
:cd
コマンドで record.idr
を書いたディレクトリまで移動します。
Idris> :cd ../Idris
そして :l
( :load
)コマンドでファイルを読み込みましょう。
ファイル名はTABによる補完が効きます。
Idris> :l record.idr
Type checking ./record.idr
*record>
プロンプトの表示が変わりましたね。record.idr
を読み込むとファイルの内容を型チェックし、REPLにロードしてくれます。
例えば Person
型のコンストラクタ MkPerson
が読み込まれてるのが確認できます。
*record> :t MkPerson
MkPerson : Int -> String -> Person
リロード
次に record.idr
を編集して incAge
を追加してみましょう
-- ...
incAge: Person -> Person
incAge = record { age $= (1+) }
これにはやり方が2種類あります。
1つは普通にエディタで編集して、 IdrisのREPLで :r
( :reload
)するものです。
*record> :r
編集
もう1つはターミナルにひきこもってる人向けにIdrisのREPLから :e
( :edit
) でファイルを編集するものです。
EDITOR
または VISUAL
環境変数に設定されているエディタを起動し、ファイルを編集するよう促します。
編集し終わるとIdrisのREPLに戻ってきて、以下のように続きます。
*record> :e
Type checking ./record.idr
Loading ./record.ibc failed: Module needs reloading:
SRC : "./record.idr"
Modified at: 2020-12-07 14:25:15.707323014 UTC
IBC : "./record.ibc"
Modified at: 2020-12-07 14:18:51.578095669 UTC
*record> :r
Type checking ./record.idr
:e
のあとに型チェックしてくれるんですが、何故かエラーになってしまいました。コンパイル結果の中間ファイル(*.ibc
) が悪さをしたのかな?しかし慌てずに :r
を打てば問題ありません。
では今読み込んだ incAge
関数を試してみましょう。
*record> incAge (MkPerson 28 "Tom")
MkPerson 29 "Tom" : Person
こうやってファイルを編集しては試しての繰り返しの作業フローができました。
継続的リロード
因みに型チェックしたいだけなら :w
( :watch
)でファイル変更を監視して継続的にリロードすることもできます。
*record> :w
record.idr
Watching for .idr changes in ["record.idr"], press enter to cancel.
Type checking ./record.idr
探索する
IdrisのREPLはIdrisが知っていることを教えてくれます。
例えば :browse
は名前空間に定義されているシンボル一覧を教えてくれます。
名前空間はまだ出てきてない機能ですが、雰囲気で察して下さい。
今回の record.idr
には名前空間を指定してなかったので Main
という名前が割り当てられています。
一覧する
:browse
コマンドで名前空間に定義されているアイテムを一覧することができます。Main
で定義されているアイテムを表示してみましょう。
*record> :browse Main
Namespaces:
Main.Person
Names:
MkPerson : Int -> String -> Person
Person : Type
incAge : Person -> Person
表示されましたね。因みに Main.Person
という名前空間もあるようです。これも表示してみましょう。
*record> :browse Main.Person
Namespaces:
Names:
age : Person -> Int
name : Person -> String
set_age : Int -> Person -> Person
set_name : String -> Person -> Person
これは record
構文で生成された関数が格納されています。
ドキュメントを表示する
Idrisには言語組み込みでドキュメントの機能があります(基本文法のところでドキュメントコメント構文を紹介しましたね)。それをREPLから表示できます。
例えば標準ライブラリの List
のドキュメントを表示してみましょう。
*record> :doc List
Data type Prelude.List.List : (elem : Type) -> Type
Generic lists
The function is: public export
Constructors:
Nil : List elem
Empty list
The function is: public export
(::) : (x : elem) -> (xs : List elem) -> List elem
A non-empty list, consisting of a head element and the rest of the list.
infixr 7
The function is: public export
しっかりと表示できてますね。 見なれない記述があってもひとまず無視して自然言語だけでも理解するようにして下さい。 まだ説明してない機能もたくさんあるので分からないことがあっても当然です。
探す
ほしい機能がないか探すこともできます。
例えば List
を結合する関数を探しているとしましょう。
その関数は List a -> List a -> List a
という型をしているはずです。これを検索してみましょう。
:s
( :search
) でその型を使って検索できます。
*record> :s List a -> List a -> List a
= Prelude.List.(++) : List a -> List a -> List a
Append two lists
= Prelude.List.reverseOnto : List a -> List a -> List a
Reverse a list onto an existing tail.
> Prelude.List.(\\) : Eq a => List a -> List a -> List a
The \\ function is list difference (non-associative). In the result of xs \\ ys, the first occurrence of each element of ys in turn
(if any) has been removed from xs, e.g.,
> Prelude.List.union : Eq a => List a -> List a -> List a
Compute the union of two lists according to their Eq implementation.
> Prelude.List.merge : Ord a => List a -> List a -> List a
Merge two sorted lists using the default ordering for the type of their elements.
....
いくつか候補がで出てきました。
このうち、 先頭に =
がついているものは完全一致、 >
がついているものはより狭い型、 <
がついているものはより広い型の関数のようです。
ひとまず最初の候補である Prelude.List.(++)
に 「Append two lists」 と書かれているのでこれが求める演算子のようです。
使ってみましょう。
*record> [1, 2, 3] ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6] : List Intege
想定どおりですね。
もう1つ探し方があります。自然言語で全文検索する方法です。こちらは :apropos
を使います。
同じく List
を結合する関数を「append」 のキーワードで検索してみましょう。
*record> :apropos append
Prelude.List.(++) : List a -> List a -> List a
Append two lists
Prelude.Strings.(++) : String -> String -> String
Appends two strings together.
Prelude.File.Append : Mode
Prelude.File.ReadAppend : Mode
Prelude.Strings.addToStringBuffer : StringBuffer -> String -> IO ()
Append a string to the end of a string buffer
Prelude.List.appendAssociative : (l : List a) -> (c : List a) -> (r : List a) -> l ++ c ++ r = (l ++ c) ++ r
Appending lists is associative.
Prelude.List.appendCong2 : (x1 = y1) -> (x2 = y2) -> x1 ++ x2 = y1 ++ y2
Appending pairwise equal lists gives equal lists
...
こちらの場合も Prelude.List.(++)
が表示されていますね。
適宜2つを使い分けながらほしい関数を探してみて下さい。
定義を表示する
:printdef
で関数の定義を表示することもできます。
*record> :printdef Prelude.List.(++)
(++) : List a -> List a -> List a
[] ++ right = right
(x :: xs) ++ right = x :: xs ++ right
恐らくソースコードに書いてあるものそのままではなくIdrisの内部表現をユーザが読める形に再出力してるんですかね?
実行する
一旦今開いてるREPLを閉じて、別のセッションを開くことにします。
REPLを閉じるには :q
( :quit
) を打ち込むかEOFを送り込む(Ctrl+D)かをします。
そして以下の内容の hello_world.idr
を用意しておきます。
main : IO ()
main = putStrLn "Hello, World"
そして idris
の引数に hello_world.idr
を与えながら起動します。
$ idris hello_world.idr
____ __ _
/ _/___/ /____(_)____
/ // __ / ___/ / ___/ Version 1.3.3
_/ // /_/ / / / (__ ) https://www.idris-lang.org/
/___/\__,_/_/ /_/____/ Type :? for help
Idris is free software with ABSOLUTELY NO WARRANTY.
For details type :warranty.
Type checking ./hello_world.idr
*hello_world>
これでファイルの中身を読み込みながらREPLが起動してくれます。
一旦REPLを起動してから :l
で読み込んでも同じですが、複数の方法を覚えておいて損はないでしょう。
さて、このファイルには main
が定義されています。
つまり実行可能です。 :exec
でファイルをコンパイ/実行できます。
*hello_world> :exec
Hello, World
:c
( :compile
) でコンパイルするこもできます。
*hello_world> :c hello_world
また、読み込んだ main
の名前を指定して実行することもできます。
:x
です。
*hello_world> :x main
Hello, World
MkIO (\w => prim_io_pure ()) : IO' (MkFFI C_Types String String) ()
ちょっと見えちゃいけないものが見えてるような気がしますが、大丈夫です。
これだけ揃えば「あれIdrisでどう書くんだっけ」というのはなんとかなるんじゃないでしょうか。
Idris小旅行
結果は貼りませんが、以下のようなコマンドでIdrisの基本の手札を確認できるんじゃないでしょうか。
Idris> :browse Builtins
idris> :browse Prelude
Idris> :browse Prelude.List
Idris> :doc Prelude.List.head'
Idris> :printdef the
Idris> :printdef id
-- などなど
まとめ
IdrisでのREPLの使い方、そしてそれを通じたIdrisの探索方法を紹介しました。 IdrisのREPLは非常に高機能で、それだけでプログラミングできてしまいます。 ぜひREPLを使いこなしましょう。