ホーム » 「動的型付け」タグがついた投稿

タグアーカイブ: 動的型付け

2023年2月
 1234
567891011
12131415161718
19202122232425
262728  

最新の投稿(電子情報)

アーカイブ

カテゴリー

コンパイラと関数電卓実験の総括

複雑な字句解析

コンパイラでは、字句解析→構文解析を行うのが一般的である…と説明をしたが、最近のC++では少し話がややこしい。

C++ではテンプレート機能などがあるので、整数型のリストみたいな型は、forward_list<int>といった書き方をする。そして、リスト型のリストを作る場合は、forward_list<forward_list<int>>という型が出てくるかもしれない。しかし、この場合、単純な字句解析処理が行われると、forward_list, “<” , forward_list , ”<” , int , “>>” というトークンに分解されることになる。しかし、これでは、ただしいC++でのテンプレート表記にならないので、少し古い C++03 では、”forward_list<forward_list<int> >”と、最後の2つの”>”の間に空白を入れる必要があった。

しかし、これはプログラム記述上問題も多いため、最新の C++11 では、”>>”と書いてもテンプレート記述の”<“との組を判断して、”>”,”>”と2つに分解してくれる。このため、字句解析の処理が lex のようなものでは不十分となっている。

形態素解析

今回の実験では、コンパイラを作るという目的で、字句解析、構文解析 を行う流れを説明し演習を行った。しかし、こういった処理は、自然言語処理でも使われている。

自然言語処理(Natural Language Processing)とは、人間の言語(自然言語)を機械で処理し、内容を抽出することです。

具体的には、言葉や文章といったコミュニケーションで使う「話し言葉」から、論文のような「書き言葉」までの自然言語を対象として、それらの言葉が持つ意味をさまざまな方法で解析する処理技術を指します。(入門編)自然言語処理(NLP)とは[引用]

今回のコンパイラの技術では、最初の処理は字句解析で説明をしていたが、日本語の場合は形態素解析が必要となる。

形態素解析 — 形態素解析とは、文法的な情報の注記の無い自然言語のテキストデータから、対象言語の文法や、辞書と呼ばれる単語の品詞等の情報にもとづき、形態素の列に分割し、それぞれの形態素の品詞等を判別する作業である。(wikipedia引用)

意味解析

自然言語処理では、これに加え構文解析の後に、意味解析の処理が行われる。例えば、「高い」という単語は、金額の意味なのか、高度の意味なのか、判断が必要だが、かかり受けする単語にお金に関するものであれば金額と判断するし、身長という単語があれば高低の意味と判断し、全体の意味を解析する。

コンパイラ処理でも、目的プログラム生成行程プログラミング言語において、コンパイラーがソースコードを解析し目的プログラムを生成する際の処理工程のひとつ。意味解析は、ソースコード内に記述された変数の型や文(ステートメント)が言語の記述仕様に沿っているかどうかをチェックする。

静的型付け・動的型付け・型推論

プログラム言語のコンパイラでも、意味解析が必要な事例として、型推論について紹介する。プログラム言語では、プログラムを記述する際に、値を記憶するために型情報が重要である。C言語では、明確に型を記述する必要がある(静的型付け言語)。これに対し、Perl , Python , PHP , JavaScript といった言語では、変数にどういった型の情報でも代入が可能となる。このため、変数宣言では型を明記する必要がないが、プログラムが動作している時点でインタプリタは型を確認しながら処理が行われる(動的型付け言語)ため、無駄な型判定処理が常に行われ処理効率が悪い。また、動的型付け言語では、型が明記されていないのでプログラムの間違いが見逃されることもある。

Microsot では、JavaScript の動的型付けの問題を解決するために、TypeScript を開発している。TypeScript では JavaScript に、静的型付けとオブジェクト指向のクラスの機能が追加されている。

プログラムを安全に作る視点であれば、データの型のチェックが行われる静的型付けはバグを減らす意味で重要であるが、プログラム記述が複雑になる問題も出てきている。例えば、C++ でのリスト処理は、forward_list のテンプレート機能を使うと、以下のように書ける。

#include <iostream>
#include <forward_list>

int main() {
  // std::forward_list<>線形リスト
  std::forward_list<int> lst{ 1 , 2 , 3 } ; // 1,2,3のリストで初期化
  // for( List* p = lst ; p != NULL ; p = p->next ) {...} に相当
  for( std::forward_list<int>::iterator itr = lst.begin() ;
       itr != lst.end() ;  
       itr++ ) {
    std::cout << *itr << std::endl ;
  }
}

しかし、繰り返し処理のためのデータ型(反復子) itr の宣言は、ただ「リストの要素で繰り返し」とい目的で書くには、型宣言が面倒すぎる。
そこで、最新の C++ では、型推論 とよばれる機能が導入され、型宣言の初期化の右辺式から変数の型を推論してくれる。あくまでも型推論は、コンパイル時に型が確定しているので、静的型付け言語の便利な機能の1つである。

#include <iostream>
#include <forward_list>

int main() {
  // std::forward_list<>線形リスト
  std::forward_list<int> lst{ 1 , 2 , 3 } ; // 1,2,3のリストで初期化
  for( auto itr = lst.begin() ; // lst.begin() の型からitrの型を推論
       itr != lst.end() ;
       itr++ ) {
    std::cout << *itr << std::endl ;
  }
}

しかし、変数の型推論をしなくちゃいけないのは、変数を使った副作用を伴う記述方法が間違いのモトという考え方では、関数型プログラミングという話が出てくる。C++のalgorithm = 関数型という意味じゃないけど…

#include <iostream>
#include <forward_list>
#include <algorithm>

int main() {
   std::forward_list<int> lst{ 1 , 2 , 3 } ;
   std::for_each( lst.begin() ,
                  lst.end() ,
                  []( int x ) { // 配列参照のコールバック関数
                     std::cout << x << std::endl ;
                  } );
   return 0 ;
}

ポインタの先には何がある?

学生さんから「ポインタの先には何があるの?」との質問があった。

私が「そのポインタの型のデータ」と答えると、さらに「ポインタはメモリの場所。でもメモリには int や char や double といった色んなデータがある。そんな色々なデータの中からデータを取り出すんだから、そこにはどんなデータが入っているのか判らないとデータを取り出せないんじゃ?」と疑問をぶつけてきた。

なるほど、本当の疑問点が見えてきた。

最近のPython等の動的型付け言語の場合

# ポインタの質問だから、C言語の場合を答えればいいんだけど…

最近の Python , PHP といった変数が型を持たない「動的型付け言語」は、まさに質問の通り。データを取り出すためには、型の情報が必要。こういう言語は、基本型以外のデータはすべて参照型(要はポインタ)なので、変数の指し示す先には型情報とそのデータがペアで保存されているので、その型情報をみてデータを取り出している。

C言語の場合(静的型付け言語)

C言語では、ポインタは単なるメモリの場所を表すだけ。ポインタの先にはデータがある。(だからデータしかないって!)

メモリからデータを読み出すときに、int 4byte で取り出すのか、 double 8byte で取り出すかどうやってわかるの?と思うかもしれないけど、そのポインタの変数がどういう型へのポインタで定義されているかプログラムを読めばわかる。それに従って取り出せばいい。こういう言語は「静的型付け言語」という。

となると「じゃあ int のデータを char として読めるの?」と思うかもしれないけど「読めるよ!」

#include <stdio.h>
int main() {
   // 型を偽って参照するのは間違いの元なので型のチェックは厳格。
   //   だから 以下の様なヤバイことをする時は、型キャスト で
   //   だますことが定番。
   int x = 0x41424344 ; 
   char* p = (char*)( &x ) ; // int型の場所をchar型にする 
   printf( "%c\n" , *p ) ;

   // int型は4バイト、次のアドレスは?
   int y[] = {
      0x11223344 , 0x12345678 ,
   } ;
   printf( "%p %p\n" , y , y + 1 ) ; 
   int *r = y + 1 ;
   printf( "%04d\n" , *r ) ; // 12345678 が表示

   // intの1byteとなりをintとして読める?
   int *q = (int*)((char*)( &y ) + 1) ;
   printf( "%04x\n" , *q ) ; // 処理系によってはメモリエラー

   // ポインタは番地を表す数値だよね?
   //  0x100番地のデータは読める?
   int* s = (int*)0x100 ;
   printf( "%d\n" , *s ) ; // Segmentation Fault.

   return 0 ;
}

さて、上記のプログラムをみてどう思った?

C言語って自由奔放で、やばくね? — ポインタなんか使えるからだよね、そう思うんなら Java 使え。ポインタなんか使えないから。

でも型宣言が面倒なんだよねPython, Ruby などの動的型付けな言語使え。

でも変数参照でいちいち型情報しらべる言語って遅くね? — あるよ。「型推論」。型を明記しなくても、プログラムの文脈から型を推論してくれる静的型付け言語。Go , Swift , Kotlin…といった、今 流行りのプログラム言語がソレ。最新のJavaやC++も型推論機能が使えるようになってるよ。んで、今話題の中学生が作ったプログラム言語 Blawn も、型推論の言語!すげーな。