6. 式と値

omakeは多くのシステムとIO関数を含んでいる、フル機能のプログラミング言語です。この言語はオブジェクト指向言語であり、数値や文字列のような基本の型もすべてオブジェクトで表現されます。しかしながら、このomake言語は3つの点において、他のスクリプト言語とは異なっています。

  • 動的なスコーピングを行います。
  • IOを除いて、この言語は全体が関数的です。この言語は代入という操作が存在しません。
  • 値の評価は通常の場合その場で行われます。これは、式が表れた瞬間に評価されるということを意味しています。

これらの機能を確かめるため、今回私たちは osh(1) omakeプログラムを使いました。 osh(1) プログラムは式を入力したら、すぐに結果が出力されるインタープリターとなっています。 osh(1) は通常シェル上で実行するために、入力された文章をコマンド文として解釈しますので、式を直接評価するためには多くの場合 value 文を使用します。

osh> 1
*** omake error: File -: line 1, characters 0-1 command not found: 1
osh> value 1
- : "1" : Sequence
osh> ls -l omake
-rwxrwxr-x  1 jyh jyh 1662189 Aug 25 10:24 omake*

6.1 動的なスコーピング

動的なスコーピングは言い換えると、変数の値が実行されているスコープ中で、もっとも近くで束縛されている変数によって決定されることを意味しています。以下のプログラムについて考えてみましょう。

OPTIONS = a b c
f() =
   println(OPTIONS = $(OPTIONS))
g() =
   OPTIONS = d e f
   f()

f()OPTIONS 変数を再定義することなく呼び出した場合、この関数は文字列 OPTIONS = a b c が出力されます。

対照的に、関数 g()OPTIONS 変数を再定義し、 f() をそのスコープ中で評価しますので、 この関数は文字列 OPTIONS = d e f が出力されます。

g の内容はローカルスコープを定義しています。 OPTIONS 変数の再定義は g についてローカルであり、この関数が終了した場合、この定義も終了します。

osh> g()
OPTIONS = d e f
osh> f()
OPTIONS = a b c

動的なスコーピングはプロジェクトでのコードを簡略化するための非常に有用なツールです。例えば、 OMakeroot ファイルでは関数の集合、 CCCFLAGS などの変数を使ったプロジェクトのビルドルールについて定義しています。しかしながら、プロジェクト中の異なったパートでは、これらの変数がそれぞれ異なった値であってほしいと思うことがあるでしょう。例えば、サブディレクトリ opt では、私たちは -O3 オプションを、サブディレクトリ debug では -g オプションを用いてビルドしたいものとします。この問題は動的なスコーピングを用いて、関数を再定義することなく、プロジェクト中の一部の変数を置き換えることができます。

section
   CFLAGS = -O3
   .SUBDIRS: opt
section
   CFLAGS = -g
   .SUBDIRS: debug

しかしながら、動的なスコーピングは欠点も持っています。はじめに、この機能はバグの箇所が分かり辛くなることがあります。具体的にいうと、あなたはプライベートにしたい変数があるとします。しかしこれはどこか別の場所で再定義される恐れがあります。例えば、あなたは以下の検索パスを組み立てるコードを持っていたとします。

PATHSEP = :
make-path(dirs) =
   return $(concat $(PATHSEP), $(dirs))

make-path(/bin /usr/bin /usr/X11R6/bin)
- : "/bin:/usr/bin:/usr/X11R6/bin" : String

しかしながら、プロジェクトのどこかで PATHSEP 変数がディレクトリのセパレータ / で再定義された場合、この関数は突如文字列 /bin//usr/bin//usr/X11R6/bin を返すようになります。あなたは明らかにそれを望んでいないのにです。

private ブロックはこの問題を解決するために用いられます。 private ブロック内で定義された変数は静的なスコーピングを用います。これは、変数の値がソーステキストのスコープ中で、もっとも最近の定義によって決定されることを示しています。

private
   PATHSEP = :
make-path(dirs) =
   return $(concat $(PATHSEP), $(dirs))

PATHSEP = /
make-path(/bin /usr/bin /usr/X11R6/bin)
- : "/bin:/usr/bin:/usr/X11R6/bin" : String

6.2 関数評価

IOを除いて、omakeのプログラムは全体が関数的です。これは二つの意味を持っています。

  • 代入という操作が存在しません。
  • 関数は引数を渡して、別の値を返す『値(value)』です。

二番目についてはそのままの説明です。例えば、以下のプログラムでは関数の値を返すことによって加算する関数を定義しています。

incby(n) =
   g(i) =
      return $(add $(i), $(n))
   return $(g)

f = $(incby 5)

value $(f 3)
- : 8 : Int

一番目については恐らく最初は困惑することでしょう。代入なしで、いったいどのようにして、サブプロジェクトにプロジェクトのグローバルな振る舞いを修正することができるのでしょうか?実際、この省略された説明は意図的にされています。サブプロジェクトが他のプロジェクトの邪魔をしないことを保障されているとき、ビルドスクリプトはより書きやすくなります。

しかしながら、サブプロジェクトが親のオブジェクトに情報を伝える必要がある場合や、内部のスコープが外部のスコープに情報を伝える必要がある場合が存在することも確かです。

6.3 環境のエクスポート

export 文によってすべて、あるいは一部の内部スコープの情報を親スコープに伝えることができます。もし引数が存在しない場合、全体のスコープの情報が親に伝えられます。さもなければ引数の変数のみが伝えられます。もっともよく使うやり方は、条件分岐中のいくつか、あるいはすべての定義をエクスポートする場合です。以下の例では、変数 B は評価された後に、2に束縛されます。変数 A は再定義されません。

if $(test)
   A = 1
   B = $(add $(A), 1)
   export B
else
   B = 2
   export

export 文が引数なしに用いられた場合は、以下のすべてが出力されます。

export 文が引数ありで用いられた場合は引数の式が評価されて、返される値は以下のようになります。

  • もし値が空であるなら、上で説明したすべてがエクスポートされます。

  • もし値が環境(environment)、あるいは部分的な環境を表現しているのなら( 詳細は “9.3.41 export” を参照してください)、対象の環境または部分的な環境がエクスポートされます。

  • そうでなければ、値は出力したい項目を指定している、文字列のシーケンスでなければなりません。また、以下の文字列は特別な意味を持ちます。

    • .RULE ー 暗黙のルールと、暗黙的な依存関係
    • .PHONY ー “phony”ターゲット宣言の集合

    すべての他の文字列は、出力する必要のある変数名として解釈されます。

例えば以下の(わざとらしい)例では変数 AB がエクスポートされて、さらに暗黙のルールはこのセクションが終わった後も、環境の中で保持されます。しかし、変数 TMP とターゲット tmp_phony は変更されずに、このセクションの中にとどまります。

section
   A = 1
   B = 2
   TMP = $(add $(A), $(B))

   .PHONY: tmp_phony

   tmp_phony:
      prepare_foo

   %.foo: %.bar tmp_phony
      compute_foo $(TMP) $< $@
   export A B .RULE

6.3.1 区域のエクスポート

この機能はバージョン0.9.8.5で導入されました。

export 文はブロックの最後で実行する必要はありません。エクスポートはブロック中のブロックも、ブロックの終わりでエクスポートされます。言い換えると、 export はその文の次にくるプログラムでも用いられます。これはコード量を減らすという点で特に有用です。以下の例では、変数 CFLAGS は両方の条件分岐文からエクスポートされます。

export CFLAGS
if $(equal $(OSTYPE), Win32)
    CFLAGS += /DWIN32
else
    CFLAGS += -UWIN32

6.3.2 エクスポートされた区域から値を返す

この機能はバージョン0.9.8.5で導入されました。

ブロックによって返された値は export を使用してもエクスポートされません。この値は普通に計算された場合、ブロック最後の状態の値として解釈されるので、エクスポートを無視します。例えば、私たちはマップの文字列をユニークな整数値に改良したテーブルを作りたいと思い、以下のプログラムを考えたとします。

# 空のマップ
table = $(Map)

# テーブルにエントリーを追加
intern(s) =
    export
    if $(table.mem $s)
        table.find($s)
    else
        private.i = $(table.length)
        table = $(table.add $s, $i)
        value $i

intern(foo)
intern(boo)
intern(moo)
# "boo = 1" と出力
println($"boo = $(intern boo)")

文字列 s が与えられると、関数 interns に既に関連付けられている値を返し、そうでない場合は新しい値を関連付けます。この場合、このテーブルは新しい値にアップデートされます。関数の初めに export をつけることによって、 table 変数はエクスポートされます。一方、 si に束縛されている値はプライベートなので、エクスポートされません。

omakeでの評価は先行して行われます。これは評価文に遭遇した場合、即座に式の評価が行われることを意味しています。この効果の一つとして、変数が定義されたときに、右側の変数定義が展開されることが挙げられます。

osh> A = 1
- : "1"
osh> A = $(A)$(A)
- : "11"

二番目の定義文の右側 A = $(A)$(A) がまず初めに評価されて、シーケンス 11 が生成されました。変数 A は新しい値として再定義されます。動的なスコーピングを用いて束縛した場合、これは従来の命令型プログラミングと同じ多くの特性を持ちます。

osh> A = 1
- : "1"
osh> printA() =
    println($"A = $A")
osh> A = $(A)$(A)
- : "11"
osh> printA()
11

この例では、出力関数は A のスコープ中で定義されます。最後の行でこの関数が呼び出されたとき、 A の動的な値は 11 であるので、この値が出力されます。

しかしながら、動的なスコーピングと命令型のプログラミングは混同すべきではありません。以下の例では違いについて示しています。二番目の printAA = x$(A)$(A)x が定義されているスコープには存在していませんので、この関数は元の値 1 を出力します。

osh> A = 1
- : "1"
osh> printA() =
    println($"A = $A")
osh> section
         A = x$(A)$(A)x
         printA()
x11x
osh> printA()
1

遅延評価式の使用で評価順序を制御する詳細については、”7.5 遅延評価式“を参照してください。

6.4 オブジェクト

omakeはオブジェクト指向型言語です。数や文字列のような基本的な値を含むすべてがオブジェクトで表現されます。多くのプロジェクトの場合、通常のトップレベルのオブジェクト中でほとんどの式が評価されるため、これを見ることはあまりありませんが、 Pervasives オブジェクトと、2,3個の他のオブジェクトが最初から定義されています。

しかしながら、オブジェクトはデータを構築するための追加手段を提供し、さらにオブジェクトを慎重に使用することで、あなたのプロジェクトをより簡単にしてくれるでしょう。

オブジェクトは以下の構文で定義されます。これは name をいくつかのメソッドと値を持ったオブジェクトとして定義しています。

name. =                     # += も同じくらいよく使います
   extends parent-object    # なくても構いません
   class class-name         # なくても構いません

   # フィールド
   X = value
   Y = value

   # メソッド
   f(args) =
      body
   g(arg) =
      body

extends 文はこのオブジェクトが指定された parent-object に継承されていることを指定します。オブジェクトは任意の数の extends 文を含めることができます。もし多くの extends 文が存在した場合、すべての親オブジェクトのメソッドとフィールドは継承されます。もし名前が衝突していた場合、前の定義は後の定義でオーバーライドされます。

class 文はなくても構いません。指定されていた場合、 instanceof 演算子を使うことでオブジェクト名を新たに定義することができるようになります。これは下で議論する :: スコープ文と同様です。

オブジェクトには任意の内容のプログラムを記述できます。オブジェクトの中に定義された変数はフィールドと定義され、関数はメソッドと定義されます。

6.5 フィールドとメソッドの呼び出し

オブジェクトのフィールドとメソッドは object.name 表記を用いて命名されます。例えば、一次元の点の値について定義してみましょう。

Point. =
   class Point

   # 通常の値
   x = $(int 0)

   # 新しい点を生成
   new(x) =
      x = $(int $(x))
      return $(this)

   # ひとつ進める
   move() =
      x = $(add $(x), 1)
      return $(this)

osh> p1 = $(Point.new 15)
osh> value $(p1.x)
- : 15 : Int

osh> p2 = $(p1.move)
osh> value $(p2.x)
- : 16 : Int

$(this) は常に現在のオブジェクトに置き換える変数です。式 $(p1.x) はオブジェクト p1x の値を呼び出します。式 $(Point.new 1)Point オブジェクトの new メソッドを呼び出す式で、初期値として15が保持された新しいオブジェクトを返します。 $(p1.move) もメソッドの呼び出しで、16が保持された新しいオブジェクトを返します。

オブジェクトは関数的であり、その場で存在しているオブジェクトのフィールドやメソッドを修正することは不可能であることに注意してください。よって、 newmove メソッドは新しいオブジェクトを返します。

6.6 メソッドのオーバーライド

1つ移動させる代わりに、2つ移動させるメソッドを持った新しいオブジェクトを作ることについて考えてみましょう。 move メソッドをオーバーライドすることでこれを実現することができます。

Point2. =
   extends $(Point)

   # moveメソッドをオーバーライド
   move() =
      x = $(add $(x), 2)
      return $(this)

osh> p2 = $(Point2.new 15)
osh> p3 = $(p2.move)
osh> value $(p3.x)
- : 17 : Int

しかし、これを行うと古い move メソッドは完全に置き換わります。

6.7 親の呼び出し

新しい move メソッドを、古い old メソッドを二回呼び出すことで定義したい場合について考えてみましょう。これは表記 $(classname::name <args>) を用いることで親を呼び出すことができます。 classname は親クラスの名前で、 name のメソッドやフィールドが関連付けられている必要があります。それでは、 Point2 オブジェクトを別の方法で定義してみましょう。

Point2. =
   extends $(Point)

   # 古いメソッドを2回呼び出す
   move() =
      this = $(Point::move)
      return $(Point::move)

最初の $(Point::move) の呼び出しは現在のオブジェクト( this 変数)を再定義していることに注意してください。なぜならこのメソッドは新しいオブジェクトを返し、二回目の呼び出しで再利用されるからです。

目次

前のトピックへ

5. 変数と名前空間

次のトピックへ

7. さらなる言語例

このページ

SourceForge.JP

SourceForge.JP

このドキュメントはsourceforge.jpのサーバを利用して提供しています。