LoginSignup
29
22

More than 5 years have passed since last update.

速習・プログラミング言語 (構文定義による差分学習)

Last updated at Posted at 2018-11-30

新たに今までとは別のプログラミング言語を学ぶ方向けに、効率のよい学習方法を紹介します。

この方法の利点

最近、GoやPromelaという言語をグループで学ぶ機会があったのですが、構文定義から学ぶ私のやり方に興味をもってくれた方がいたので書いてみました。私のやり方では、まず学ぶ言語の「構文定義」を探し、そこからどんなプログラムをかけるか考えるというものです。

この「構文定義」とは、コードをどのような構造で書くかを決めた規則のことです。このあと具体例を出しますが、この構文定義とは「if文はこう書くよ」といった規則だと思ってください。

そして、この構文定義を読むと、どのようなコードを書けるのか(あるいは書けないようにされているのか)をすぐに把握できるという利点があります。例えば、構文定義に「async」や「await」といった文字列が現れれば、非同期処理を同期的な見た目で扱う機能の存在を把握できます。他にも構文定義の量から、その言語がどれだけ多機能なのかを把握できます。

ただし、構文定義からわかるのは、あくまでどのような見た目のコードを書けるかということだけです。そのため、そのコードが実際にどのような意味をもつかについてはわかりません。そこで、構文定義を一通り眺めた後は、それぞれの構造の名前(例えば「if文」のような)から意味を推測するか、その名前で検索するといった学習が続きます。言い換えると、既存の知識との差分だけの学習で、ほぼ言語仕様の全容を把握できるということです1。つまり、この学習方法はさまざまな言語に触れた人ほど、学習速度をあげられるという特徴をもっています

これからこの方法を紹介していくのですが、いきなり現実の構文定義から出発すると難しすぎるので、ミニ言語の例からみていきましょう。

構文定義の読み方

ここでは次のような単純なコードを書けるミニ言語と、その構文定義を例に、構文定義がどのようなものなのかを説明していきます:

if a==b {
  // DO SOMETHING
}

なんとなく挙動のわかりそうなミニ言語ですね。では、このコードで使われている構文定義をみてみましょう。表の左に構文定義、右にその読み方を書いてみました。

構文2 読み方
プログラム := 文* プログラム は、複数の のまとまり。
文 := if文 │ コメント文 if文コメント文 のどちらか。
if文 := "if" 式 "{" 文* "}" if文if の後に があり、次に { が続いて、複数の のまとまりが続いて、} で終わる。例えば、 if a == 0 { } と書ける。
コメント文 := "//" [A-Z ]* "\n" コメント文// の後に英数大文字か空白が続いて改行で終わる。
式 := 比較式 このミニ言語の とは 比較式 のこと(現実的には四則演算や関数呼び出しなどがここに入る)。
比較式 := 変数名 "==" 変数名 比較式変数名 の後に == が続き、変数名 で終わる。
変数名 := [a-z]+ 変数名 は、aからzまでの英小文字からなる文字列。

では、この構文定義から、先ほどのプログラムと構文定義の対応をみていきます。まず、コード全体は プログラム です。

Step1
<プログラム>

プログラム は複数の からなるため、プログラム と複数を意味する * で書き換えます(プログラム文*)。

Step2
<文>*

この if文コメント文 のいずれかと書き換えられますが、ここでは元のコードにあわせて1つの if文 で書き換えます(文*if文)。

Step3
<if文>

次に、if文 の構文定義にしたがって内容を展開しましょう(if文if { 文* })。

Step4
if <式> {
  <文>*
}

ここで を展開できますが、まず の方を展開してしまいましょう(変数名 == 変数名)。

Step5
if <変数名>==<変数名> {
  <文>*
}

この 変数名 ですが、元のコードを読むとそれぞれ ab が対応します(変数名a変数名b)。

Step6
if a==b {
  <文>*
}

最後に複数の を1つの コメント文 で書き換えます(文*コメント文)。

Step7
if a==b {
  <コメント文>
}

元のコードにあうように内容を展開すると、復元できました(コメント文//[a-z ]*\n)。

Step8
if a==b {
  // DO SOMETHING
}

ここまでの例で、構文定義がどのようなもので、そして実際のコードとどのように対応するのかがわかったと思います。では、実際の言語で同じように構文定義をみていきましょう。この題材として「サンプルコードの意味を把握する」と「適当なコードを書いてみる」の2つを解説します。

サンプルコードの意味を把握する

今回は、次のような Go のコードの意味を調べてみましょう(このコードはGoでテストを書いてみよう - golang.tokyo コードラボから引用しています)。このうち、(f ClockFunc) の部分がわからなかったとします:

func (f ClockFunc) Now() time.Time {
        return f()
}

では、まずGo の構文定義をみにいきましょう。そして、先ほどのコードのうち、特徴的なキーワードである「func」でページ内を検索します。

すると、300件の件数がヒットしますが、これを順番にみていきます。すると30番目ぐらいで次のような記述を見つけられます:

More than one type may implement an interface. For instance, if two types S1 and S2 have the method set

func (p T) Read(b Buffer) bool { return … }
func (p T) Write(b Buffer) bool { return … }
func (p T) Close() { … }

これを読む限り、どうやらメソッドと関連がありそうなことがわかりました。そこで、メソッドの宣言文を確認しにいきます。宣言は英語で「Declaration」なので、「Method declaration」に類似する項目を上部の索引から探します。

すると、すばり「Method declarations」という項目があるので、それをみにいきます。Method declarations の構文定義は次のようになっているようです:

MethodDecl = "func" Receiver MethodName Signature [ FunctionBody ] .
Receiver   = Parameters .

Parameters     = "(" [ ParameterList [ "," ] ] ")" .
ParameterList  = ParameterDecl { "," ParameterDecl } .
ParameterDecl  = [ IdentifierList ] [ "..." ] Type .

IdentifierList = identifier { "," identifier } .

ここから、(f ClockFunc) はどうやら Receiver と呼ばれていて、f は Identifier(識別子)、ClockFunc は Type(型)と呼ばれていることがわかります。ここまでくればしめたもので、オブジェクト指向言語に触れたことがあれば、Receiver の意味するものを推測できます。言い換えると、Swift/Python でいうところの self、Java/JavaScript でいうところの this、Ruby でいうところの @ にあたるものだとあたりをつけられます。少し独特なのは、Receiver の仮引数名らしきもの(f にあたる部分)を自由に設定できそうなところです。しかし、同じように Receiver を自由に命名できる Python を触ったことがあれば、なんとなく納得できます。

この学習の過程が、まさしく他の言語との差分で学習しているという部分です!なんとなく雰囲気を掴めたでしょうか。

では、次の例をみてみましょう。

適当なコードを書いてみる

先ほどは、与えられたサンプルコードを読むために構文定義を使いました。今回は、コードを書く際に構文定義を使ってみます。この題材として、モデル検査用の言語 Promela で、なにがしかのループ文を書いてみるとします3

それでは、Promela の構文定義をみにいきます(Promela (Spin 6.1) の構文定義)。まず、ここから探すのは一番最初にどのような構文(トップレベルの構文と呼びます)を使えるかです。よくあるトップレベルの構文名は「Program」や「Package」などですが、Promela の構文定義にはそのような名前はないようです。

また、他にもよくあるパターンとして、文のまとまりがトップレベルの構文に含まれているというものがあります。例えば、if文やfor文などはプログラムの出だしから書けることが多いですが、これはトップレベルの構文が複数の文のまとまりだからです。そこで、文を意味する「Statement」でページ内検索してみると、次のように stmnt という構文定義が文を意味していそうなことがわかります:

stmnt   : IF options FI     /* selection */

    ...(省略)...

    | name ':' stmnt    /* labeled statement */

    ...(省略)...

ここから、この stmnt を含められる構文を再帰的に探します。これ以上遡れなくなる構文が、探しているトップレベルの構文にあたります。まず、stmnt を含められるのは step のようです4

step    : stmnt [ UNLESS stmnt ]
    | decl_lst
    | XR varref [',' varref ] *
    | XS varref [',' varref ] *

この step は、次の sequence に含められるようです:

sequence: step [ ';' step ] *

さらにこの sequenceproctype に含められることがわかります5

proctype: [ active ] PROCTYPE name '(' [ decl_lst ]')'
      [ priority ] [ enabler ] '{' sequence '}'

そして、この proctypemodule に含められるようです:

module  : proctype  /* proctype declaration */
    | init      /* init process       */
    | never     /* never claim        */
    | trace     /* event trace        */
    | utype     /* user defined types */
    | mtype     /* mtype declaration  */
    | decl_lst  /* global vars, chans */

この modulespec に含められるようです:

spec    : module [ module ] *

この spec は、その他のどの構文にも含められないようですから、これが探していたトップレベルの構文です!

このトップレベルの構文がわかれば、あとは前にミニ言語と同じような構文定義の書き換えでコードを記述できます。まずは、トップレベルの構文である spec を配置します:

<spec>

この spec は、少なくとも1つ以上の module で書き換えができるようです。module が何者なのかいまのところわかっていませんが、たかがループ文を書くのに複数のモジュールが必要になるとも考えづらいので、1つの module で書き換えます(specmodule [ module ]*):

<module>

moduleにはさまざまなものを書けるようですが、今回は事前に proctype っぽいという情報を周囲のサンプルコードから掴んでいました(ずるい)。そのため、proctype で書き換えます(moduleproctype):

<proctype>

さて、この proctype が曲者で、次のように謎の属性(active やら enabler やら)を指定できるようですが、既存の知識ではさっぱり意味がわかりません:

proctype: [ active ] PROCTYPE name '(' [ decl_lst ]')'
      [ priority ] [ enabler ] '{' sequence '}'

そこで、この proctype における active がどのような意味を持つか調べます。たまたま Promela の構文定義は構文名がリンクになっていたので、謎の active のリンク へ飛んでみます。すると次のような解説が書かれていました:

NAME
active - prefix for proctype declarations to instantiate an initial set of processes.
SYNTAX
active proctype name ( [ decl_lst ] ) { sequence } 
active '[' const ']' proctype name ( [ decl_lst ] ) { sequence }
DESCRIPTION
The keyword active can be prefixed to any proctype declaration to define a set of processes that are required to be active (i.e., running) in the initial system state. At least one active process must always exist in the initial system state. Such a process can also be declared with the help of the keyword init .

これを読む限り、なにやら proctype の初期状態を指示できるようです。そして、少なくとも1つは active な proctype がないといけないとも書いてありますから、今回は指定してみることにします(エラーが出たらまた考え直す方針):

active proctype <name> ( [ <decl_list> ] ) [ priority ] [ enabler ] {
    <sequence>
}

同様に、priorityenabler を調べ、これが省略しても問題なさそうとわかるので、これらは省略することにします:

active proctype <name> ( [ <decl_list> ] ) {
    <sequence>
}

次に name を調べますが、これはどうやらただの名前で、特に解説もなさそうなのでどんなものでも指定できると推測して適当な名前にします:

active proctype P ( [ <decl_lst> ] ) {
    <sequence>
}

そして、残るは decl_lst です。この構文定義を調べると、次のように型名(typename)らしき構文なのがわかります:

decl_lst: one_decl [ ';' one_decl ] *
one_decl: [ visible ] typename  ivar [',' ivar ] *

ここから、C++でいうテンプレート宣言、Javaやその他の言語でいうジェネリクス宣言あたりを連想します。今回はただのループ文を書きたいだけですので、省略してみます:

active proctype P() {
    <sequence>
}

あとは、sequence にfor文を埋め込むだけです!ここから少し速をあげますが、sequencestepstmntFOR '(' range ')' '{' sequence '}'(for文らしきもの)へと書き換えていきます:

active proctype P() {
    for ( <range> ) {
        <sequence>
    }
}

ここで、for文にリンクが貼られていることに気づくので、その内容を確認してみます。すると、次のように親切にも例文が載っていました!:

NAME
for - deterministic iteration statement.
SYNTAX
for '(' varref ':' expr '..' expr ')' '{' sequence '}' 
for '(' varref in array ')' '{' sequence '}' 
for '(' varref in channel ')' '{' sequence '}'
DESCRIPTION
The for statement was added in Spin Version 6 to simplify writing common loops over a user-defined range of values, the elements of an array, or the messages currently stored in a channel. For statements are internally converted into the corresponding Promela code, with the first statement issued being an assignment statement. This means that for statements are always executable (the guard statement is an assignment), independent of what the guard of the sequence in the body of the for is. Execution could of course still block inside the body of the for statement.
EXAMPLES
The simplest form of the for statement is to simply iterate the execution of a given sequence of statements for a range of values, which must consist of two expressions separated by the two character token '..'. For instance,
int i;

for (i : 1 .. 10) {
    printf("i = %d\n", i)
}

ここから、どうやらループ変数の事前宣言が必要そうだということを把握できます。最終的には、次のようなコードになりました:

active proctype P() {
    int i

    for (i : 0 .. 10 ) {
        printf("%d", i)
    }
}

そして、恐る恐るこれを実行してみると、ちゃんと意図通りに実行できました!どうやらうまく書けていたようです。

ここ注目してほしいのは、私たちは Promela のチュートリアルを一切やらずにここまでのコードを書けたということです。チュートリアルをやれば、すでに知っている知識もたくさん出てくることを思えば、今回の学習過程はとても効率的なことがわかりますね。

まとめ

このように、構文定義から言語を学ぶことで、既存の知識からの差分だけで言語を効率的に学習できます。このように、構文定義に興味を持って新しい言語を学んでみてはいかがでしょうか。

なお、構文定義の応用などもっと知りたい!という方には「事前知識なしで理解する 静的検査のいろは」というスライドをオススメしておきます(自薦)。

最後に、主要な言語(主観)の構文定義を付録として残しておきます。

付録: 主要な言語の構文定義


  1. ここで把握できるのはあくまで仕様だけです。したがって、標準ライブラリやコミュニティの慣例、仕様の落とし穴などは把握できないという欠点があります。これを補うために有効な方法として、標準ライブラリの一覧を眺めたり、lint のような静的解析ツールのルールを読むという裏技があります。 

  2. この構文定義の記法は、正規表現からの連想でなんとなく読めるゆるふわなやつです。より形式化された記法では BNF とか ABNF とか EBNF とか色々ありますが、現実にはメジャーな言語で BNF に似たオレオレ記法がいろいろ登場するので、あまり厳密に考えなくて大丈夫です。 

  3. 実際には、何かでみたサンプルコードがまどろっこしい定義だったので、疑問に思って他の書き方を調べたという経緯でした。 

  4. なお、stmnt 自身に stmnt を含められるようですが、これはトップレベルの構文を探す上では無視して構いません。理由を説明します。自分自身を含められるパターンには2つの可能性がありえます。(1) トップレベルの構文が自身を含めるパターン、(2) トップレベル出ない構文が自身を含めるパターン、です。今回重要視しているのは、stmnt がトップレベルの構文かどうかですが、step から stmnt を参照できる以上、stmnt がトップレベルの構文にはなりえません。そのため、パターンとしては (2) なので無視して構わないというわけです。 

  5. なお、proctype 以外にも stmntoptions にも含められるようですが、先ほど stmnt はトップレベルの構文ではないということがわかっており、options も同様にトップレベルの構文ではないことがわかります。したがって、proctype のみが今のところのトップレベルの構文の候補になっています。 

29
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
22