BETA

Dotty でユーザ定義 Let 式

投稿日:2020-03-29
最終更新:2020-03-29

TL;DR

これの解説記事を書いてみました。

ラムダ式で Let 式

Excel が最近実装したことで話題の Let 式ですが、ラムダ式を利用すれば比較的簡単に同じようなものを作ることができます。
例えば以下のような感じです。

def let [T, R] (value: T)(body: T => R): R = body(value)  

let (10) { x =>  
  println(x)  
}  

x というローカル変数に 10 を束縛した状態で println(x) を計算することができるので、これは Let 式と言えます。

しかしこれではなんとなく面白くありません。
なんというか、if 文 (と遅延評価) を利用して unless 文を実装するみたいな、そりゃできるよね感があります。
Let 式の実装で問題となるのはどうやってローカル変数を表現するかということなのに、この Let 式はラムダ式の変数束縛の機能をそのまま利用してしまっているからです。

Dotty の Context Function を利用する

Dotty には Context Function という機能があり、これを利用することでローカルな範囲でのみ利用可能なメソッドを実装することができます。
例えば以下のようにすることで、与えられた引数を x という変数に束縛して利用する Let 式のようなものが実現できます。

class LetXContext [T] (val value: T)  
def letx [T, R] (value: T)(body: LetXContext[T] ?=> R): R = body(using new LetXContext(value))  
def x [T] (using ctx: LetXContext[T]): T = ctx.value  

letx (10) {  
  println(x)  
}  

LetXContext[T] ?=> R が Context Function の型で、LetXContext[T] が Context (文脈) の型を表しています。つまりこの型は『 LetXContext[T] という文脈の下で R 型となるような型』と読みます。この実引数では文脈 LetXContext[T] で利用可能なメソッドを利用することができます。
文脈 LetXContext[T] で利用可能なメソッドというのは、using 指定された LetXContext[T] 型の引数を取るメソッドです。例えば x メソッドがそれに当たります。using 引数はそのメソッドが使われている場所のスコープから適切な値を自動的に取り出します。取り出される値は Context Function の呼び出し側で using 指定した実引数として与えられます。つまりここでは new LetXContext(value) となるため、letx で与えられた値 valuex で利用される値として引き回されることになります。

Dynamic と組み合わせる

Scala には Dynamic という特殊な trait が存在します。
この trait を実装したクラスのメソッドを呼び出すと、そのメソッド名が見つからなかった場合に代わりに特別なメソッド selectDynamicapplyDynamic が呼び出されます。
これを利用することで、上の例では x 限定だった変数名をどんな名前でも利用可能にできます。

最終的な実装は以下のようになります。

import scala.language.dynamics  

class LetContext [L <: Singleton, T] (val value: T) {  
  def in [R] (f: LetContext[L, T] ?=> R): R = f(using this)  
}  
object let extends Dynamic {  
  def applyDynamic [L <: Singleton, T] (tag: L)(value: T): LetContext[L, T] = {  
    new LetContext(value)  
  }  
}  
object $ extends Dynamic {  
  def selectDynamic [L <: Singleton, T] (tag: L)(using ctx: LetContext[L, T]): T = ctx.value  
}  

let a 10 in {  
  let b $.a + 1 in {  
    println($.b)  
  }  
}  

letDynamic trait を実装したシングルトンオブジェクトで、applyDynamic メソッドを持ちます。
applyDynamic メソッドは引数を取るメソッド呼び出しの代わりに呼び出され、第一引数にはメソッド名が、第二引数以下にはメソッド呼び出しの実引数が渡されます。
例えば、let.a(10) というメソッド呼び出しは let.applyDynamic("a")(10) というメソッド呼び出しとなります。
この場合 applyDynamic の型引数 L には "a" の型が入るわけですが、ここでの型は Singleton Literal Type となります。つまり、"a" 以外の文字列にはマッチしない型です。
この型引数 LLetContext の型引数として引き渡され、文脈として利用されることになります。

$ は同じく Dynamic trait を実装したシングルトンオブジェクトで、selectDynamic メソッドを持ちます。(selectDynamic には必ずレシーバが必要なのでこの $ はなくせません。import による省略もできませんでした)
selectDynamic メソッドは引数を取らない形式のメソッド呼び出しの代わりに呼び出され、メソッド名が引数として渡されます。
例えば、$.a というメソッド呼び出しは $.selectDynamic("a") というメソッド呼び出しとなります。
ここで、selectDynamic メソッドは using のついた LetContext[L, T] 型の引数を取るため、文脈 LetContext[L, T] の下でしか呼び出すことができません。
したがってこのメソッドは let の中でしか呼ぶことができず、また型引数 L が一致する必要があるため正しい名前かどうか検査されることになります。

$. をなくすことができないのが残念ですが、かなりそれっぽくなったのではないでしょうか。
このようにして Dotty では変数束縛を持つような言語機能をマクロを使わずにユーザ定義することが可能です。
読者の方も是非触ってみてください!

リファレンス

Dotty Documentation

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@phenanの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう