JavaScriptで関数型言語を作ろう(7) 型の表現・式の型チェック

今回は珍しく，[前回](https://qiita.com/hoge1e3/items/5319c3b258b61a78c185) の理解度チェックの答え合わせから．

前回，こんなプログラムに対して`HelloWorld`を出力させるところまでは確認できました．

~~~
"Hello".add("World")
~~~

また，次のように存在しないメソッドを含むプログラムを実行させると，`SemanticError: addd  is not defined`のような意味エラーをちゃんと出してくれる，というところも確認できていると思います．

~~~
"Hello".addd("World")
~~~

では，次はどうでしょう．確か，前回「文字列にはaddしかなくて，subやらmulやらは使えない仕様です」と断っておいたので，エラーになってほしいところです．

~~~
"Hello".sub("World")
~~~

こうなりました：

~~~
    const {Num, Str}=runtime;
    return Str("hello").sub(Str("world"));

TypeError: Str(...).sub is not a function
    at eval (eval at run (C:\bin\Dropbox\workspace\tinyfunc\index.js:42:18), <anonymous>:5:25)
    at run (C:\bin\Dropbox\workspace\tinyfunc\index.js:43:17)
    at test (C:\bin\Dropbox\workspace\tinyfunc\index.js:16:9)
    (略)
~~~
やった！エラーが出た！　と思った方は残念でした．これ，前回我々がちゃんと仕込んだ`SemanticError`ではありません．その証拠に

~~~
    const {Num, Str}=runtime;
    return Str("hello").sub(Str("world"));
~~~
というコードが生成されてしまっています．`SemanticError`が出た場合，その場で例外となるので，コードが生成されない点に注意しましょう．

このプログラムにおいては「コードがされちゃって，実行したら実行時エラーになった」にすぎず，処理系がちゃんと事前にエラーをチェックできていません．

では，なぜ`"Hello".addd("World")`はちゃんと`SemanticError`になったのに，`"Hello".sub("World")`は我々の仕込んだエラーチェックをすり抜けて実行されて（実行時エラーになって）しまったのでしょうか．

ポイントは`Semantics.ts`です．

~~~~lang/Semantics.ts
const numberTypeMembers=new Set(["add","sub"]);
export function check(expr: ValueExpression) {
    const E=(...messages:any[])=>new SemanticError( ...messages);
    console.log("Checking", expr);
    if (expr instanceof NumberLiteral) {
    } else if (expr instanceof StringLiteral) {//追加
    } else if (expr instanceof Identifier) {
    } else if (expr instanceof MemberAccess) {
        check(expr.left);
        //ここで存在しないメンバーのチェックをしている
        if (!numberTypeMembers.has(expr.name.text)) {
            throw E(expr.name.text, " is not defined");
        }
    } else if (expr instanceof Call) {
        check(expr.left);
    } else {
        throw invalid(expr);
    }
}
~~~~

この`        if (!numberTypeMembers.has(expr.name.text)) {`
の部分で，`.add`やら`.addd`やら`.sub`やらがメンバ名として正しいかどうかチェックしているのですが，`numberTypeMembers`の中身が
`const numberTypeMembers=new Set(["add","sub"]);`
，つまり「`add`か`sub`ならOK，それ以外は`SemanticError`」というチェックをしています．（この部分は理解度チェックをしている方はもう少し増えてますね）

しかし，文字列型には`add`しかないので，`sub`がOKのわけがないんですが，この処理ではOKにしてしまっています．

そもそも文字列型のメンバーのチェックで`numberTypeMembers`を使ってはまずいですよね．

では，どうするのか，というのが今回のテーマです．

# 式の型チェック

~~~~typescript
    } else if (expr instanceof MemberAccess) {
        check(expr.left);
        //ここで存在しないメンバーのチェックをしている
        if (!numberTypeMembers.has(expr.name.text)) {
            throw E(expr.name.text, " is not defined");
        }
~~~~

exprがMemberAccess ，つまり`a.b`のような形の式の場合の処理だけ抜き出してみました．
MemberAccessには，`a.b`の`a`の側を`left`，`b`の側の名前を`name`で管理しています．

- `expr.left`の部分に数値が来ていれば`add`やら`sub`やら，他にも`mul`やら`div`やらも使ってよい
- `expr.left`の部分に文字列が来ていれば`add`以外はダメ

という場合分けが必要になってきます．

そこで，`expr.left`の部分がどんな型なのかを調べる関数を新たに定義しましょう．`Semantics.ts`の適当なところ（`check`メソッドの上あたり）に`getType`という関数を作成しましょう．

~~~typescript
export function getType(expr: ValueExpression):Type {
    //式exprの型を返す    
}
~~~

で，これはエラーになります．

~~~
lang/Semantics.ts:5:48 - error TS2304: Cannot find name 'Type'.

5 export function getType(expr: ValueExpression):Type {
                                                 ~~~~
~~~

`Type`なんていう型はないですよね．はい，では今から作りましょう．`lang/Types.ts`というファイルを新規作成しましょう．

~~~lang/Types.ts
import { Identifier } from "./Expressions";

export class StructType {
    constructor(public name:Identifier, public members:Member[]) {}
}
export class Member {
    constructor(public name:Identifier) {}
}
export type Type=StructType;
~~~

`StructType`って何かというと，今問題になっている数値型や文字列型を表します．`struct`というとC言語の「構造体」を思い出すので，数値なのに構造体？　と思うかもしれませんが，この言語では，`3.add`みたいに，数値でも構造体っぽく「ドット＋名前」でアクセスできるので，構造体扱いにしています．

この`.add`の部分を「メンバー」と呼びます．`StructType`には複数のメンバーを定義でき，それらが`members`というフィールドに格納されます．

`Member`の定義は，とりあえず名前だけです．型やメンバーの名前はExpressions.tsで定義した`Identifier`を使っています．

最後の行で「`Type`は，`StructType`のことです」，と書いてあります．後で`StructType`以外のものも`Type`の仲間として追加される予定です．

では，これを使って，数値型や文字列型を定義します．

~~~lang/Types.ts
//今まで書いたものの下に追加
const nMembers=[
    new Member(new Identifier("add")),
    new Member(new Identifier("sub")),
];
export const numberType=new StructType(new Identifier("Number"), nMembers);
const sMembers=[
    new Member(new Identifier("add")),
];
export const stringType=new StructType(new Identifier("String"), sMembers);
~~~~

そして，これを使って，Semantics.tsのgetTypeを定義してみましょう．

~~~lang/Semantics.ts
import { Type, stringType, numberType } from "./Types";
//中略
export function getType(expr: ValueExpression):Type {
    if (expr instanceof StringLiteral) {
        return stringType;
    } else {
        return numberType;
    }
}
//後略
~~~~

とりあえず，文字列リテラル（`"abc"`とか）は文字列型，それ以外は数値型にしておきましょう（「おい，そんなんで大丈夫か」と思った方，お見事．大丈夫じゃないです！　理解度チェックで大丈夫じゃない例を挙げてみましょう）

そして，`StructType`からメンバーを取り出す関数も作っておきます．なお，指定した型の指定した名前のメンバーがない場合は`SemanticsError`にしています．

~~~~lang/Semantics.ts
//中略
export function getMember(type:StructType, name:Identifier):Member {
    for (const member of type.members) {
        if (member.name.text===name.text) return member;
    }
    throw new SemanticError("member is not defined");
}
//後略
~~~~

最後に，`check`の部分を書き換えましょう．

~~~lang/Semantics.ts
//中略
export function check(expr: ValueExpression) {
    const E=(...messages:any[])=>new SemanticError( ...messages);
    console.log("Checking", expr);
    if (expr instanceof NumberLiteral) {
    } else if (expr instanceof StringLiteral) {//追加
    } else if (expr instanceof Identifier) {
    } else if (expr instanceof MemberAccess) {
        check(expr.left);
        const leftType=getType(expr.left);//expr.leftの型を取得
        getMember(leftType, expr.name);//その型からメンバーを取得
    } else if (expr instanceof Call) {
        check(expr.left);
    } else {
        throw invalid(expr);
    }
}
//後略
~~~

では，これで実行してみます（tscを忘れないように）．

まず数値型

~~~test/single.txt
3.add(2)
~~~
↑これは正しく実行されて5になります

~~~test/single.txt
3.sub(2)
~~~
↑これもOK（以前の理解度チェックでちゃんと引き算の仕組みを作っていれば）

~~~test/single.txt
3.addd(2)
~~~
↑これはちゃんと`SemanticsError`になってくれるのでOK．

~~~test/str.txt
"hello".add("world")
~~~
↑これは正しく実行されてhelloworldになります

~~~test/str.txt
"hello".addd("world")
~~~
↑これはちゃんと`SemanticsError`になってくれるのでOK．

さて，今回問題になっていた（`SemanticsError`にならなくて，実行時エラーになっちゃうもの）を実行してみましょう．

~~~test/str.txt
"hello".sub("world")
~~~


~~~~
(中略)
Checking StringLiteral { value: 'hello' }
SemanticError: SemanticError: member is not defined
    at getMember (C:\bin\Dropbox\workspace\tinyfunc\lang\Semantics.js:23:11)
    at check (C:\bin\Dropbox\workspace\tinyfunc\lang\Semantics.js:38:9)
    at Object.check (C:\bin\Dropbox\workspace\tinyfunc\lang\Semantics.js:44:9)
    at run (C:\bin\Dropbox\workspace\tinyfunc\index.js:36:17)
~~~

ちゃんとコードが生成されず，`SemanticsError`になりました．

# 理解度チェック

(1) メンバーが見つからなかったときのエラーを親切にしてみましょう：

Before:

~~~
SemanticError: SemanticError: member is not defined
~~~

After:

~~~~
SemanticError: SemanticError: member 'sub' is not defined in type 'String'
~~~~

(2)　この状態でも相変わらず「`SemanticsError`にならなくて，実行時エラーになっちゃうもの」は存在します．どんなコードでしょう？　前回まで書いたコードの中にヒントがあるかもしれません．


[ここまでのソースコード](https://github.com/hoge1e3/tinyfunc/releases/tag/typecheck-1)