Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
0
Help us understand the problem. What is going on with this article?
@hoge1e3

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

今回は珍しく,前回 の理解度チェックの答え合わせから.

前回,こんなプログラムに対して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"]);
,つまり「addsubならOK,それ以外はSemanticError」というチェックをしています.(この部分は理解度チェックをしている方はもう少し増えてますね)

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

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

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

式の型チェック

    } 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.baの側をleftbの側の名前をnameで管理しています.

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

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

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

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にならなくて,実行時エラーになっちゃうもの」は存在します.どんなコードでしょう? 前回まで書いたコードの中にヒントがあるかもしれません.

ここまでのソースコード

0
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
hoge1e3
HTML5で動作するゲームエンジン+言語「Tonyu System2」を開発しています.

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
0
Help us understand the problem. What is going on with this article?