本記事は「IGGG Advent Calendar 2019」の10日目の記事です.

表題の通り,TaPL という書籍で紹介されているプログラミング言語の実装例を Elm でやってみたという話です(その3). その1その2はこちら.

  • 第4章 算術式のML実装
  • 第7章 ラムダ計算の ML 実装
  • 第10章 単純型のML実装 (本記事はココ)
    • 7章のを型付きラムダ計算にする
  • 第17章 部分型付けの ML 実装
  • 第25章 System F の ML 実装
    • 最後に型の多相性を追加

実装は全て下記のリポジトリにあげています:

また,今まで同様にWeb ブラウザから遊べるようになってます

第10章 単純型の ML 実装

さて,いよいよみんな大好き「型」の登場だ. 10章は少し面白くて,4章で実装した算術式の真偽値に関する部分と7章の型なしラムダ計算を組み合わせて,更にそれに型をのせるプログラミング言語を実装する. つまり,今までの実装をちゃんとやっていれば割とサクッとできています.

なお,同期各位は全然やらなかったせいか10章を5週ぐらいやっている笑.

構文規則

t := x       [変数]
   | \x:T.t  [ラムダ抽象]
   | t t     [関数適用]
   | true
   | false
   | if t then t else t

v := \x:T.t
   | true
   | false

T := Bool    [真偽値型]
   | T -> T  [関数型]

ラムダ抽象には型注釈(:T の部分)がある. なんで付けるのかとかは9章に書いてあるのでぜひ TaPL を買って読んでください(おい). これを Elm の型として実装する:

type Term
    = TmVar Int Int
    | TmAbs String Ty Term
    | TmApp Term Term
    | TmTrue
    | TmFalse
    | TmIf Term Term Term

type Ty
    = TyArr Ty Ty -- Arrow の Arr ね
    | TyBool

isval : Context -> Term -> Bool
isval _ t =
    case t of
        TmAbs _ _ _ ->
            True

        TmTrue ->
            True

        TmFalse ->
            True

        _ ->
            False

type alias Context = List ( String, Binding )
type Binding = NameBind

型の型 Ty 以外は,4章と7章の Termisval を合体させているだけだ.

評価規則

// 7章の評価規則
 t1 => t1'
---------------
 t1 t2 => t1' t2

 t2 => t2'
---------------
 v1 t2 => v1 t2'

 (\x:T.t12) v2 -> [x|-> v2]t12

// 4章の評価規則
 if true then t2 else t3 => t2

 if false then t2 else t3 => t3

 t1 -> t1'
-------------------------------------------------
 if t1 then t2 else t3 => if t1' then t2 else t3

評価規則も同様に型注釈の構文が追加されただけでほとんど変わらない. 変わらないということはすなわち,実行時(評価)には型の有無は影響しないということだ. 構文規則同様,4章と7章の実装を組み合わせることで実装が終わる:

eval : Context -> Term -> Maybe Term
eval ctx t =
    if isval ctx t then
        Just t
    else
        Maybe.andThen (eval ctx) (eval1 ctx t)

-- あらゆる TmAbs のパターンマッチに Ty のパラメーターを追加する必要はある
eval1 : Context -> Term -> Maybe Term
eval1 ctx t =
    case t of
        TmApp (TmAbs x ty t12) t2 ->
            if isval ctx t2 then
                Just (termSubstTop t2 t12)
            else
                Maybe.map (TmApp (TmAbs x ty t12)) (eval1 ctx t2)

        TmApp t1 t2 ->
            Maybe.map (flip TmApp t2) (eval1 ctx t1)

        TmIf TmTrue t2 _ ->
            Just t2

        TmIf TmFalse _ t3 ->
            Just t3

        TmIf t1 t2 t3 ->
            eval1 ctx t1 |> Maybe.map (\t1_ -> TmIf t1_ t2 t3)

        _ ->
            Nothing

-- t に s を代入する
termSubstTop : Term -> Term -> Term
termSubstTop s t = ...

型付け規則

ここからが新しい. いわゆる型検査のことだ. TaPL では「正しく型付けされた項はおかしくならない」という性質(安全性・健全性ともいう)について議論されている(8章で). 例えば,これから定義する型検査が通った項(Term)は eval 関数を適用しても無限ループなどにはならない. さて,そのための型付け規則は次のようになっている:

// 真偽値の型付け規則
 true : Bool

 false : Bool

 t1 : Bool    t2 : T    t3 : T
-------------------------------
 if t1 then t2 else t3 : T

// 単純ラムダ計算の型付け規則
 x : T ∈ Γ
-----------
 Γ ⊢ x : T

 Γ, x : T1 ⊢ t2 : T2
-----------------------------
 Γ ⊢ \x : T1 . t2 : T1 -> T2

 Γ ⊢ t1 : T11 -> T12    Γ ⊢ t2 : T11
-------------------------------------
 Γ ⊢ t1 t2 : T12

ここで新しく出てくる Γ は型環境と言い,変数と型の対応関係を線形リストのような感じに保持している. Γ ⊢ t : T というのは「型環境 Γ のもと項 t は型 T に型付け可能」という風に読める(たぶん). まぁ実装してみればわかる(ほんとか?):

-- 型環境には Context を再利用する
type alias Context = List ( String, Binding )
type Binding
    = NameBind
    | VarBind Ty -- 変数の型を保持

-- 項 t の型が最終的に導出できれば型付け可能ということになる
typeof : Context -> Term -> Maybe Ty
typeof ctx t =
    case t of
        TmVar x _ ->
            -- Context から型情報を引っ張ってくる
            getTypeFromContext ctx x

        TmAbs x ty1 t2 ->
            let
                -- Context に 変数と型の対応を追加する
                ctx1 = addbinding ctx x (VarBind ty1)
            in
            -- ラムダ抽象は中の項 t2 が型付け可能である必要がある
            typeof ctx1 t2
                |> Maybe.map (\ty2 -> TyArr ty1 ty2)

        TmApp t1 t2 ->
            case ( typeof ctx t1, typeof ctx t2 ) of
                ( Just (TyArr ty11 ty12), Just ty2 ) ->
                    -- 関数適用の場合は引数の型 ty11 と適用する項の型 ty2 が同じである必要がある
                    if ty11 == ty2 then
                        Just ty12
                    else
                        Nothing
                _ ->
                    Nothing

        TmTrue ->
            Just TyBool

        TmFalse ->
            Just TyBool

        TmIf t1 t2 t3 ->
            case ( typeof ctx t1, typeof ctx t2, typeof ctx t3 ) of
                ( Just TyBool, Just ty2, Just ty3 ) ->
                    -- if-then-else の場合は t2 と t3 の型が同じである必要がある
                    if ty2 == ty3 then
                        Just ty2
                    else
                        Nothing
                _ ->
                    Nothing

getTypeFromContext : Context -> Int -> Maybe Ty
getTypeFromContext ctx idx =
    case getbinding ctx idx of
        Just (VarBind ty) ->
            Just ty

        _ ->
            Nothing

getbinding : Context -> Int -> Maybe Binding
getbinding ctx n =
    case ( ctx, n ) of
        ( [], _ ) ->
            Nothing

        ( ( _, bind ) :: _, 0 ) ->
            Just bind

        ( _ :: next, _ ) ->
            getbinding next (n - 1)

addbinding : Context -> String -> Binding -> Context
addbinding ctx x bind =
    ( x, bind ) :: ctx

REPL で試してみる:

> import TaPL.Chap10 as Chap10 exposing (Term(..), Ty(..), Binding(..))
-- (\x : (Bool -> Bool) . (\f : (Bool -> Bool -> Bool) . f x)) (\x : Bool . x)
> Chap10.typeof [] (TmApp (TmAbs "x" (TyArr TyBool TyBool) (TmAbs "f" (TyArr (TyArr TyBool TyBool) TyBool) (TmApp (TmVar 0 2) (TmVar 1 2)))) (TmAbs "x" TyBool (TmVar 0 1)))
Just (TyArr (TyArr (TyArr TyBool TyBool) TyBool) TyBool)
    : Maybe Ty
-- (\x . x x) (\x . x x) はうまく型付けできない
> Chap10.typeof [] (TmApp (TmAbs "x" (TyArr TyBool TyBool) (TmApp (TmVar 0 1) (TmVar 0 1))) (TmAbs "x" (TyArr TyBool TyBool) (TmApp (TmVar 0 1) (TmVar 0 1))))
Nothing : Maybe Ty

文字列に変換

基本的に4・7章の定義を利用すれば良いのだが,型注釈ができるようになったので型も変換できるようにする:

display : Term -> String
display t =
    printtm [] t
        |> Maybe.map (dropIfStartsWith "(") -- 最初と最後のカッコを消している
        |> Maybe.map (dropIfEndsWith ")")
        |> Maybe.withDefault ""

printtm : Context -> Term -> Maybe String
printtm ctx t =
    case t of
        TmAbs x ty t1 ->
            let -- 重複しない変数名を生成して Context に積む
                ( ctx1, x1 ) = pickfreshname ctx x
            in
            Maybe.map
                (\s1 -> String.concat [ "(\\", x1, " : ", printty ty, ". ", s1, ")" ])
                (printtm ctx1 t1)
        ... -- あとは同じ

printty : Ty -> String
printty ty =
    case ty of
        TyArr ty1 ty2 ->
            String.concat [ "(", printty ty1, " -> ", printty ty2, ")" ]

        TyBool ->
            "Bool"

-- Context から重複する変数名を探し ' を追加する
pickfreshname : Context -> String -> ( Context, String )
pickfreshname ctx x = ...

REPL で試す:

> Chap10.display (TmApp (TmAbs "x" (TyArr TyBool TyBool) (TmAbs "f" (TyArr (TyArr TyBool TyBool) TyBool) (TmApp (TmVar 0 2) (TmVar 1 2)))) (TmAbs "x" TyBool (TmVar 0 1)))
"(\\x : (Bool -> Bool). (\\f : ((Bool -> Bool) -> Bool). (f x))) (\\x : Bool. x)"
    : String

パーサー

これも同様に4・7章の実装を合わせるだけ:

module TaPL.Chap10.Parser exposing (parse)

import Parser exposing ((|.), (|=), Parser)
import TaPL.Chap10 exposing (Term(..), Ty(..))

type alias Context =
    { env : Dict String Int
    , depth : Int
    }

iniCtx : Context
iniCtx =
    { env = Dict.empty, depth = 0 }

parse : String -> Result (List Parser.DeadEnd) Term
parse =
    Parser.run parser

parser : Parser Term
parser =
    termParser iniCtx |. Parser.end

termParser : Context -> Parser Term
termParser ctx =
    Parser.oneOf
        [ parParser ctx -- 括弧のパーサー
        , absParser ctx -- ラムダ抽象のパーサー
        , valParser     -- ture/false のパーサー
        , ifParser ctx  -- if-then-else のパーサー
        , varParser ctx -- 変数のパーサー
        ] -- 関数適用のパーサーだけ分けてるのは左再帰対策(その2参照)
        |> Parser.andThen (appParser ctx)

言わずもがな,型注釈のパースをする必要があるので,absParser はその2のと若干異なる:

absParser : Context -> Parser Term
absParser ctx =
    Parser.succeed Tuple.pair
        |. Parser.symbol "\\"
        |. Parser.spaces
        |= Parser.lazy (\_ -> varStrParser)
        |. Parser.spaces
        |. Parser.symbol ":"            -- ここから
        |. Parser.spaces
        |= Parser.lazy (\_ -> tyParser) -- ここまでが追加
        |. Parser.spaces
        |. Parser.symbol "."
        |. Parser.spaces
        |> Parser.andThen (absParserN ctx)

-- 型のパーサー
tyParser : Parser Ty
tyParser =
    Parser.oneOf
        [ value "Bool" TyBool
        ]
        |. Parser.spaces
        -- これも左再帰対策(その2参照)
        |> Parser.andThen tyArrParser

-- 関数型のパーサー
-- 関数型 T->T は右結合(T->T->T は T->(T->T) となる)
tyArrParser : Ty -> Parser Ty
tyArrParser ty =
    Parser.oneOf
        [ Parser.succeed (TyArr ty)
            |. Parser.keyword "->"
            |. Parser.spaces
            |= Parser.lazy (\_ -> tyParser)
        , Parser.succeed ty
        ]

REPL で試してみる:

>  "(\\x : Bool . if x then x else (\\f : Bool -> Bool -> Bool . f x x) (\\x : Bool . \\y : Bool . y)) (if true then false else true)"
|   |> Parser.parse
|   |> Result.toMaybe
|   |> Maybe.andThen (\t -> Chap10.typeof [] t |> Maybe.map (always t)) -- 型検査
|   |> Maybe.andThen (Chap10.eval [])
|   |> Maybe.map Chap10.display
|   
Just "false" : Maybe String

完璧だ.

おまけ: SPA

前回整理したので基本的に足していくだけだ. ただし,型検査を Calculus に加える必要がある:

module TaPL.Calculus exposing (..)

type alias Calculus ctx t ty =
    { parse : String -> Result (List Parser.DeadEnd) t
    , typeof : Maybe (ctx -> t -> Maybe ty) -- 追加
    , eval1 : ctx -> t -> Maybe t
    , display : t -> String
    , init : ctx
    , logs : List t
    }

eval1 : Calculus ctx t ty -> Maybe (Calculus ctx t ty)
typecheck : Calculus ctx t ty -> Bool
parse : String -> Calculus ctx t ty -> Result (List Parser.DeadEnd) (Calculus ctx t ty)
display : Calculus ctx t ty -> List String

あとは chap10 の定義も追加するだけ:

module TaPL exposing (..)

type Chapter
    = Chap0
    | Chap4 (Calculus () Chap4.Term Never)
    | Chap7 (Calculus Chap7.Context Chap7.Term Never)
    | Chap10 (Calculus Chap10.Context Chap10.Term Chap10.Ty)

init : String -> Chapter
init s =
    case s of
        "chap4" -> ...

        "chap7" -> ...

        -- 追加
        "chap10" ->
            Chap10
                { eval1 = Chap10.eval1
                , display = Chap10.display
                , parse = Chap10.parse
                , init = []
                , logs = []
                , syntax = Chap10.syntax
                , typeof = Just Chap10.typeof
                }

        _ ->
            Chap0

parse : Chapter -> String -> Result (List Parser.DeadEnd) Chapter
eval1 : Chapter -> Maybe Chapter
typecheck : Chapter -> Bool
display : Chapter -> List String

あとはこれらを Main でいい感じに呼び出すだけ.

おしまい

次回はいつになることやら.