はじめに
自分なりに検証したメモに近いので、あんまり役に立たない記事です。
まとめ
-
func
- 副作用ありの場合にコンパイルエラーになってくれる。
- ただし、仮引数がポインタ(
var
やptr
)や、参照型(ref
)の場合は、この仮引数を変更しても検知はしてくれない。
-
not nil
- 指定した型に
nil
を代入される可能性がある場合にエラーになる。
- 指定した型に
func 及び noSideEffectプラグマ
公式マニュアルは以下のとおり。(Google翻訳さん)
要するに副作用がないと定義しているのに、副作用があった場合はコンパイルエラーを出してくれる機能の模様。
noSideEffectプラグマは、副作用を持たないようにproc / iteratorをマークするために使用されます。つまり、proc / iteratorはそのパラメータから到達可能な場所のみを変更し、戻り値は引数にのみ依存します。そのパラメータが型var T、ref T、またはptr Tをいずれも持たない場合、これはロケーションが変更されないことを意味します。コンパイラがこれを検証できない場合にproc / iteratorに副作用がないとマークするのは静的エラーです。
特別な意味規則として、組み込みのdebugEchoは副作用がないように見せかけるので、noSideEffectとしてマークされたルーチンのデバッグに使用できます。
funcは副作用のないprocの糖衣構文です。
func
は{.noSideEffect.}
のシンタックスシュガーなので、基本的にはfunc
を用いればよいと思われる。
では、この機能「ほんまか?」を検証する。
echoやファイルI/Oするケース
func fileIOFunc(filename:string) = filename.fileWrite("Side Effect")
# Error: 'fileIOFunc' can have side effect
うむ、ちゃんとエラーになる。
関数外の変数の参照するケース
let i = 0 #iへは再代入はできないが、エラーになる。
func varFunc(arg:int):int = i + arg
# Error 'varFunc' can have side effect
うむ、ちゃんとエラーになる。
ちなみに上記のlet i = 0
をconst i = 0
に変更すると、コンパイルエラーにはならない。
const i = 0
func varFunc(arg:int):int = i + arg # Ok
呼出すプロシージャが副作用をもっているケース
let i = 0
proc subProc(arg:int):int = i + arg
func callProcFunc(arg:int):int = subProc(arg)
# Error: 'callProcFunc' can have side effect
ふむ、ちゃんとエラーになる。
引数を変更しちゃうケース
func varArgFunc(arg:var int):int =
arg = arg + 1
return arg
var i = 1
echo varArgFunc(i) # 2
コンパイルエラーにはならず。これは前述の
そのパラメータが型var T、ref T、またはptr Tをいずれも持たない場合
に該当しないので、副作用ありと判断してくれない模様。ここまで検知はしてくれないか・・・。ちょっと残念?。ref T
型を引数に渡すことは普通にありえるがこんなもんなんだろうか?まぁ、わざわざvar
をつけるんだから、そりゃそうか・・・という気もしますが。
not nil制約
Kotlinのスマートキャスト機能はスバラシイ。最近のプログラミング言語はNull安全がモダンらしい。
Nimにもnot nil
というキーワードを型に指定することができ、nilを許容しない型を定義できる。
not nil利用例
import strformat
# ドキュメントに記載ないが、実験的機能のためプラグマが必要
{.experimental. "notnil".}
type
# Nil許可型
NilablePoint = ref object
x,y:int
# Nil不許可型
Point = NilablePoint not nil
# 以下の様に継承関係持たせたかったが、うまくいかなかった・・・
#Point = ref object of NilablePoint not nil
proc `$`(self:NilablePoint):string = fmt"x:{self.x},y:{self.y}"
let p1:Point = nil # nil不許可なのでコンパイルエラー
let p2:NilablePoint = Point(x:1,y:1) #これはコンパイル通る。
let p3:Point = NilablePoint(x:1,y:1) #これもコンパイル通る。
echo p2
echo p3 # $(toString)はNil許可型にバインドされているが、Nil不許可型でもうごく。
まず、KotlinのようにNil不許可型 -> Nil許可型
の継承関係を作ろうとしたが、それをするとなぜかNil不許可型にnilを代入してもコンパイルエラーにならなかった。
おそらく、親の型の特性を引き継ぐ方が優先されている?(ちょっと、良くわからない挙動。。。)
なので仕方なく、Nil許可型のエイリアスとしてNil不許可型を定義して、not nil
制約をつけてみると、nilを代入すると正しくコンパイルエラーになってくれた。
nilを返し得るプロシージャとの関係
次にnilを返し得るプロシージャの戻り値を、Nil不許可型で受けたとき
# nilを返しうるプロシージャ
proc nilableProc(n:int):NilablePoint=
if n > 0 :
NilablePoint(x:n,y:0)
else:
nil
# Nil不許可型にバインドされるプロシージャ
proc showX(self:Point) = echo(self.x)
let p1:Point = nilableProc(1) # Error: cannot prove 'nilableProc(1)' is not nil
let p2:NilablePoint = nilableProc(1) # これは通る
p2.showX # これは通らない #Error: cannot prove 'p2' is not nil
エラーが出てほしいところでは、期待通り以下のようなコンパイルーエラーが発生してくれるので、Nil安全な模様。
Error: cannot prove 'nilableProc(1)' is not nil
(Nilにならないと証明できない)
で、使いやすいのか?
Kotlinと比べると煩雑&中途半端な気がします。実装の仕方によってヌルポを軽減はできるけれど、完全抑止は難しそう。
基本的にはNil許可(あえてnot nil
と付けないといけない)なんで、今の所オマケっぽい機能に感じました。
まだ「実験的」機能のため、今後に期待。