こちらの記事を拝見して初めて「Numbat」を知ることとなりました。F#の測定単位に関心していた身としてはとんでもない感動を受けたのですが、いかんせん調べても資料が少ない (公式のドキュメントも不十分に感じる) ので自分で調べて書かせていただきます。
間違った部分あると思いますがもしご指摘ありましたらご連絡ください
あとNumbatのコードハイライトが存在しないので、似ている言語Rustのコードハイライトを使用します。
単位システムは神
公式のドキュメントはこちらからご覧ください。
Numbat の動作・パラダイム
静的型付けの、おそらくスクリプト言語(?)。内部的に Rust 製の仮想マシンを持っており、入力されたコードをこの仮想マシンのオペコードの列(バイトコード)に変換して実行する仕組みのようです。
コードを見ていると特に構文については Rust 言語の影響が強く感じられ、パラダイムは強めの関数型プログラミングっぽい印象が強かったです。後述しますが関数とかリストの扱い方とかは特に。
ソースコードの拡張子は .nbt
です。
言語文法
コメント
#
を配置した場所はその場所から行末までコメントになります
# ここはコメント
基本型
いわゆる構造体とかじゃない基本型
単位については後述します
Bool
真偽値。リテラルは true
と false
。
Scalar
無単位の数値。Numbatは整数と浮動小数点を区別しないようです。
リテラル
面白いのは「10のN乗」を10e5
みたいに書けるとこと、8進数表記があるところ、ちょっとおもしろい文字が使えるとこでしょうか
# 整数
123
# 値が大きい場合はは途中でアンダーバーで区切れる
12_345
# この場合、`12345`と書いたのと同じ意味
# 区切り位置に制約はない
1_2345
# 実数、型としては上記の整数と同じ `Scalar`
0.123
# 先頭の0は省略可
.123
# 10の指数表記の e が使用可能、この場合 `6.02 * 10 ^ 23` の意味
6.02e23
6.02e+23
# 指数には負数も使用可
10e-5
# 1/4 を表すUnicode定義の文字、`0.25`として解釈されます。まじかよ
¼
# 16進数表記
0xFF
# 8進数表記
0o34
# 2進数表記
0b10
# Not a Number
NaN
# 無限
inf
String
文字列です。
# 文字列リテラル
"Hello World"
# 文字列補間 : `{}`でくくった部分は式として評価されます。
let x=10
"x is {x}"
# 文字列補間には Rust の書式指定が有効
"{pi:0.2f}"
定数(変数)
まずNumbatには変数が存在しません。1
最近は可変な変数を明示的な宣言をしない限り認めない言語も多く(Rustとかまさにそうだし)、そもそもNumbat自身が関数型プログラミングの影響を強く受けている様子なのでそういった意味でも変数を導入しなかったのだと思います。
これに関連し、Numbatには代入演算子は存在しません。
代わりに値に名前を付けて束縛することは可能で、定数と呼ばれているようです。
let x = 0.1
# Numbatは定数への再代入を認めません したがって次の式は有効ではありません
x = 10
# これも無効
x += 10
# 値の変更が必要なら新たに定数の宣言が必要です
let varx = x + 10
演算子
論理演算子
一般的な記法だと思います
論理否定 | ! |
!(a == 1) |
論理和 | || |
x || y |
論理積 | && |
x && y |
ビット演算子
ソースコードを漁ったりもしたけど、どうもシフト演算子やビット演算子はないらしい?2
そもそもVMのオペコードにも含まれてないようだし
まあもともと科学的計算を目的とする言語だからビット演算は必要ないのでしょう。
そもそも整数と実数が区別されない関係上追加しづらいのもあると思います。
数値演算子
珍しいものは特別に後述します
JuliaとかもそうですがNumbatはAsciiコード範囲にない文字も一部演算子として有効なようです。
階乗の計算が演算子で用意されてるのは珍しいですね。
単項演算子
用途 | 演算子 | 例 |
---|---|---|
符号反転 | - |
-x |
階乗 | ! |
5! |
二項演算子
用途 | 演算子 | 例 |
---|---|---|
加算 | + |
x + y |
減算 | - |
x - y |
乗算 |
* , · , ×
|
x * y , x · y
|
除算 |
/ , ÷ , per
|
x / y , x ÷ y
|
累乗 |
^ , **
|
x ^ y , x ** y
|
比較 |
< , <= , > , >= , == , !=
|
x > y , x != y
|
三項演算子
後で説明しますが条件分岐のif式と同じもののはずです
用途 | 演算子 | 例 |
---|---|---|
分岐評価 | if then else |
if x then y else z |
特殊な演算子
per
除算演算子として使用可能です
# 次は `4 / 2` と等価、値としては 2 を返す
4 per 2
暗黙的乗算
複数の値を空白で区切ると乗算演算子の適用と同じ結果を与えます。
let x=5
# 次は `4 * x` と等価、値としては 20 を返す
4 x
累乗 (Unicode Input)
²
, ³
, ⁻¹
などといった、Unicode定義の上付き文字を使用して累乗表現が可能です。
# 次は `4 ^ (-1)` と等価、値としては 0.25 を返す
4⁻¹
パイプライン演算子 (逆順関数呼び出し)
演算子 |>
によって、 関数を適用する順に左から評価させることができます。
通常 Numbat は関数の部分適用ができません3が、パイプライン演算子に直接与えている関数にだけ部分適用が有効になります。3
let x = 10
fn add(x, y) = x + y
# `add(10, x)` と同じ意味
x |> add(10)
# 複数連続で連結できる
[0, pi, 2 pi] |> map(cos) |> map(acos)
# 一方でこういう部分適用は無効
[0, 1, 2] |> map(add(10))
関数
fn
キーワードを使用した行が関数定義になります。最初に挙げた記事の方もおっしゃってたけどめっちゃRustっぽい。主に fn
キーワードが。
# 型を省略した場合ジェネリクスを使った汎用関数として解釈されます
fn add(x,y) = x + y
# 完全な型注釈
fn add(x : Scalar, y : Scalar) -> Scalar = x + y
# ジェネリック型注釈
fn sum<A>(x: A, y: A) -> A = x + y
# 型制約
# DimはDimensionの意で、次元を持つ量を指します
# Scalar, Time, Length など、Bool や Stringを含まない
# 平たく言うと数値
fn sum<A:Dim>(x: A, y: A) -> A = x + y
# 再帰関数
fn fib(n) =
if n ≤ 2
then 1
else fib(n - 2) + fib(n - 1)
# where句によって式中の定数を後置きで記述することも可能
fn func(x) = x + constant1 + constant2
where constant1 = 10 + 20
and constant2 = 20 * 30
なお、関数の本体は常に単一の式であり、中に複数の式・文を連続で記述する形の定義は使用できません。4
// こういうやつ
int func(void){
int x = 0;
x += 10;
...
}
計算式が複雑になる場合はwhere
を使ってくれってことだと思います。
いかにも関数型言語らしい関数の扱い方って感じ
プロシージャ
一部の言語組み込みの関数(?)をプロシージャと呼んでいるようです。
なんでプロシージャを関数と区別しているのか
多分プロシージャは式じゃなくて文なんだと思います。3
通常関数型プログラミング言語においてほとんどの記述は式であり、何らかの値を返すものですが、その点で値を返さないこれらの処理を関数と呼ぶべきとして扱うべきではないとして呼び分けているものだと思われます。
実際に次のような記述は無効になります。
# typeプロシージャの結果は定数に束縛できない
let x = type(1)
print
, assert
, assert_eq
, type
があります。
print
プロシージャ
画面に出力を行います。そりゃそうよな
# `1` を出力
print(1)
# 文字列の補間を使うと便利
let x = 10
print("The value of x is {x}")
# 文字列補間の話ですがRustの書式指定が使えます
# この場合は `3.14` が出力される
print("{pi:0.2f}")
assert
プロシージャ
Bool
の入力に対して値が false
の場合、エラーを発生させます。
let x = 10
# error : assertion failed
assert(x == 1)
assert_eq
プロシージャ
2つの入力に対してすべての値が等値でない場合、エラーを発生させます。
let x = 10
# error : Assertion failed
assert_eq(x, 1)
3つ目の引数を与えた場合、その値は2値の許容誤差になります。
let x = 10
# `x` と `10.5` の誤差が `0.1`
# ↓
# 誤差が第三引数の `1.0` 以下
# ↓
# 問題なし
assert_eq(x, 10.5, 1.0)
# error : Assertion failed
assert_eq(x, 10.5, 0.1)
type
プロシージャ
式の型を評価します
# Scalar
type(1)
# Length
type(1.2 m)
# Time
type(3 s)
デコレータ
デコレータはコード中の要素に対して付加情報を付け加えることができます。
多分 C#やRustの属性、Javaでいうところのアノテーションと似た機能
@example("add(10,20)")
fn add(x, y) = x + y
デコレータ自体の追加の宣言はできません3。
(公式ドキュメントにデコレータの説明がないので公式が「できない」って言ったわけではありませんが、
ソースコード中にデコレータはRustのenum
として定義されてるのが確認できた5のでソース自体をいじらない限り増やせないと思います)
デコレータには引数(?)を与えることができます。(できないものもあります)
識別子 | 用途 | 引数 |
---|---|---|
metric_prefixes |
単位に対するメートル法接頭辞の有効化 | なし |
binary_prefixes |
単位に対するIECの2進接頭辞の有効化 | なし |
aliases |
単位の別名の宣言 | 別名の識別子(複数可)、および注釈(後述) |
url |
参照先URLの指定 | URL : 文字列 |
name |
(コード中の識別子ではない実際の)名前の指定 | 名称 : 文字列 |
description |
要素の説明 | 説明 : 文字列 |
example |
コード例の記述 | 記述例 : 文字列 |
単位
これで必要な説明は済みました。ようやく単位の話ができる
Numbat の最大の特徴です。なので詳しめに書きます。
値は型としてその値の次元(単位)をとることができます。
F# の測定単位が似た機能として挙げられますが、この言語ではある値の型は基本的にその物理的な単位というか具体的な単位ではなく、次元として扱われます。
ここで「次元」というのはその値の「種類」、つまりそれが「時間」なのか、「質量」なのか、「長さ」なのか、といった話です。
一方で「単位」というのはある次元の値の表現方法、つまりある値の次元が「長さ」であるのを前提として、その値は「メートル」なのか「フィート」なのか「自分の腕の長さ」なのかという話です。
(実際にNumbatでは長さの単位として「自分の腕の長さ」を定義することもできます!それが有意義なのかは別の話ですけど)
例えば、時間の値の型は Time
、長さの値の型は Length
として扱われます。seconds
とか metre
とかではなく。
一方で値を記述するとき、取り出すときは具体的な単位を使用できます。
# これはメートル単位で記述された 型 `Length` の値
3.0 m
# これはグラム単位で記述された 型 `Mass` の値
2.4 g
# これは秒単位で記述された 型 `Time` の値
9.58 s
全てのSI(国際単位系)の単位や一部のSI併用単位は事前に定義されています。また、SIでない単位についても多くのものが事前に定義されており使用可能です。
なんかすごいのも混じってるけど
# 例えば馬力
10 horsepower
10 hp
# オンス
10 ounce
10 oz
# 天文単位
1 au
1 astronomicalunit
# 「センチメートル」 や 「年」「トン」 などももちろん使用可能
1 cm
3 year
2 ton
# スムート (https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%A0%E3%83%BC%E3%83%88)
# 「マサチューセッツ工科大学のスムートさんの1958年10月時点の身長」だそうです
# ジョーク単位だけど標準ライブラリの units::humorous モジュールに実際定義されてる
1 smoot
ちなみに見ての通り単位に別名を使用することもできます。(後述)
また、単位は使用時に複数組み合わせることもできます。
# 速度次元の単位 `フィート毎時`
1 ft / hour
# per 演算子も有効
1 ft per hour
# エネルギー次元の単位 `ニュートン・メートル`
1 N m
# もちろん、積演算子も有効
1 N * m
定数や式の値を特定の単位で取り出すときは単位変換演算子を使用します。
以下のいずれかが使用可能です。左辺に変換対象、右辺に変換先の単位を置きます。
->
→
➞
to
# 3.28084 ft
1 m -> ft
# 86400 s
1 day to sec
異なる単位で演算する場合自動的に単位をそろえて計算が行われます。
# 4.85058 day
123890 s + 2 day + 34 hour -> day
値は定数に格納した場合も維持されますがその定数の型は次元になります。
let x = 10 m
# Length
type(x)
計算途中は単位を意識する必要はなく、入力するときと出力するときだけ指定すればあとはよしなにしてくれるわけです。
単位の宣言
新たに単位を宣言する場合、その単位の次元と基準値の指定が必要になります。
「ITF (国際テニス連盟) による認定ボールの仕様」 によれば、テニスボールの質量は一個当たり最小で$56.0g$なんだそうです。これを質量の単位としてみましょう。
この場合次のように定義します。
# 次元「Mass」 の 単位「テニスボール」
unit tennisball : Mass = 56.0 g
この時 1 tennisball == 56.0 g
となります
ちなみにここでは単位の次元として Mass
を明示的に指定していますが、これは単位 g
から推論できるので省略可能です。
ところでNumbat標準ライブラリには「月の質量」lunar_mass
が extra::astronomy
モジュールに定義されています。この値は次元がMass
、質量なので同じ質量次元を持つ単位 tennisball
に変換できるはずです。つまり...
unit tennisball : Mass = 56.0g
use extra::astronomy
lunar_mass -> tennisball
# = 1.31107e+24 tennisball [Mass]
つまり「 月の質量はテニスボール $1.31 \times 10^{24}$ の質量と同じ 」だったのです! ナ、ナンダッテー!!
この計算が有意義かどうかは置いといて、とにかくこのようにして新しい単位の定義と変換を計算できます。
基本単位と派生単位
Numbat には 基本単位(base unit) と派生単位 (derived unit)が存在します。
基本単位とはその単位単体で宣言された単位です。Numbat だと 単位second
は次元Time
の基本単位です。
基本単位はその単位の次元だけをもって宣言されます
unit second: Time
一方で、ほかの単位との関係を含めて宣言される単位を派生単位と呼びます。例えば単位 minute
は時間次元の基本単位である second
との関係を持つ派生単位です。
この場合次のように宣言されます。
unit minute: Time = 60 second
この時 1 minute == 60 second
の関係が成り立つとして次元が宣言されます。
なお、一つの次元に対して基本単位を複数宣言することは可能ですが、この場合二つの基本単位を元とする単位系の間に関連を定義できない3ため、単位系を跨ぐ変換をしようとするとエラーが発生します。
次元の宣言
次元自体を新たに宣言することもできます。例えばここでは線密度を定義するとしましょう。
線密度 とはある単位長さ当たりの質量を示す単位です。主にケーブルやワイヤみたいな細長いものの重さを表すときに使われることがあります。
(よく言う単位である密度は単位体積当たりの質量です)
さて、線密度は単位長さ当たりの質量なので次のような次元を持つはずです。
Mass / Length
この時線密度 Linear Density
は次の様にして宣言できます。
dimension LinearDensity = Mass / Length
これだけで完了です。これ以降 Numbat は次元 Mass / Length
のことを LinearDensity
という名前で扱ってくれます。
もちろんほかの次元を元としない次元を新たに作ることも可能です。
dimension TheDimensionName
ちなみに次元を指定せずに単位を宣言した場合にも自動的に次元が追加されます。
# 例えば次のように書いた場合、単位 `something`と同時に、
# 次元 `Something` も自動的に導入されます。
unit something
# こう書いたものとして解釈される
unit something : Something
高度な次元指定
次元宣言には他にも次のような構文が使用できます。
# ある次元に反比例する次元は 1 に対する除算であるかのように記述できる。
dimension Frequency = 1 / Time
# ちなみにここでの `1` は数値ではなくキーワードのような扱いなので
# 代わりに `2` を置くとかみたいなことは不可能
# よってこれは無効
dimension Frequency = 2 / Time
# 2次元の積も使用可能
dimension Momentum = Mass * Velocity
# ある次元の累乗に比例する要素があればそのように書ける
dimension Acceleration = Length / Time^2
メートル法接頭辞
メートル法において、単位 m
は接頭辞 k
(キロ) や m
(ミリ)などといった接頭辞を付けることができます。
新たに宣言する単位にこの記法を認める場合、@metric_prefixes
デコレータを使用します。
@metric_prefixes
unit second: Time
この記述によって millisecond
や kilosecond
などが自動的に単位として有効になります。
ちなみに ギビバイト(GiB) とか メビバイト (MiB)とかの方についてる接頭辞(2進接頭辞)は @binary_prefixes
で有効になります。
別名の指定
単位に別名を付ける場合@aliases
デコレータを使用します。
例えば、次のように記述すれば単位 meter
の別名として metre
, m
が有効になります。
@aliases(metre, m: short)
unit meter: Length
特に短い省略名として使われるべき名前については short
で注釈をつけることで明示に指定することができます。
以上で単位終わり!あとはウイニングランです
条件分岐(if 式)
if ~ then ~ else
式が使用可能で、評価する式を分岐します。
let x = 13
# `13` を返す
if x > 10 then x else 10
なお、これも例によって関数と同じく評価するものは単一の式でなければならないので、C みたいな言語のif分と完全に同じ使い方は認めていません。
というかブロック(複文)がないと言うべきなのでしょうか
反復
調べた範囲だと、プログラミング言語の構文として提供される、いわゆる反復文は存在しないようです。6
これも関数型プログラミングらしい言語仕様ですね。
複数回の計算を行う場合は、おそらく再帰関数かリストに対する map
関数の適用になると思います。
追記
ただし一応while
やfor
は予約語として登録はされているそうです。今後使えるようになるのかは未明ですが...
リスト
組み込みのデータ型としてリストが使用可能です
おもに記法 [...]
で生成できます。
# 型 List<String>
["a", "b", "c"]
# 型 List<Scalar>
[1, 2, 3]
ただし、リストは異なる型(単位)の値を許容しません。
# 無効、時間次元の値と長さ次元の値は同時にリストに入れられない
[1m, 10s]
リスト処理用関数の例
let list = [1, 2, 3]
# 要素数の取得
# = 3
len(list)
# いくつかの組み込み関数があります
# = 6
sum(list)
# = 2
mean(list)
# map関数 : 第一引数の値をすべての値に適用して処理結果のリストを返す
# = [1, 4, 9]
map(sqr,list)
# filter関数 : 条件を満たした要素だけのリストを返す
# = [1, 2]
fn less_than_3(x) = x < 3
filter(less_than_3, list)
# range関数 : 指定した値から指定した値までを含む連続した整数のリストを返す
# = [1, 2, 3, 4, 5]
range(1, 5)
# 指定した値から指定した値までを指定した数で等間隔に分割したリストを返す
# = [0 m, 0.25 m, 0.5 m, 0.75 m, 1 m]
linspace(0 m, 1 m, 5)
小ネタ
https://github.com/sharkdp/numbat/blob/master/numbat/modules/core/lists.nbt
これリスト関連関数の定義なんですが、ほんとに関数型プログラミング言語なんだなって感じます。リストの結合とか特に
構造体
構造体、たぶんこれ読んでる人は今更説明しなくてもよさそうですが...。複数の要素をまとめて一つの型として定義します。
ほぼRustです。
ただ、フィールドは例によって可変じゃないので代入はできません。
# 構造体の定義
struct Vector {
x: Length,
y: Length,
}
# 構造体の生成
let position = Vector { x: 6 m, y: 8 m }
# 要素のアクセス
position.x
# 代入は無効
position.x = 12 m
あと構造体にジェネリクスは使えません。7ジェネリクスは関数に固有の機能らしい。
リストはジェネリクスみたいな表記(List<String>
みたいに)されるけど特別扱いでしょうか?
モジュール
この言語の要素はモジュールという単位に分けられます。
モジュールの扱いについてですがほぼRustです。
あれと同じように、モジュールの識別子はファイルの識別子とフォルダの識別子に基づいて設定されます。
例えば、モジュールが格納されているフォルダから見て./core/math/statistics.nbt
というパスにファイルがあった場合このモジュールは core::math::statistics
になります。
この場合次のように書くとそのモジュールが実行されます。
use core::math::statistics
要素が読み込まれるとかじゃなくて、指定した Numbat ソースコードの実行がなされます。
結果としてそのファイルに関数や定数が書かれていれば、そのコードの実行によってその環境にそれらの要素が定義されるので、コード中でその関数なり定数なりを使用可能になります。
その特性上、Rustに存在する pub
キーワードのような要素の公開範囲の設定・制限の機能は存在しないようです。
また、use
文において特定の要素(関数や定数、単位など)だけインポートするようにする記述も存在しません。3
# こういうやつ
use module::submodule::{Struct}
で、その「モジュールが格納されているフォルダ」とやらはどこになるのかですが、これは公式ドキュメントのここに記載があります。
モジュールの検索対象のフォルダは複数挙げられていますが、Windows環境だとその一つは例えば、C:\Users\{user_name}\AppData\Roaming\numbat\modules
以下になります。
プレリュード
Rustと同じくコード起動時に読み込まれるモジュール群(正確にはそのモジュール群の実行が記述されたNumbat ソース)が存在し、プレリュードと呼ばれます。
コマンドライン引数 -N
または --no-prelude
で無効化できます。
(どうでもいいけどプレリュードって名前かっこいいよね)
外部言語呼び出し
調査中。
一部の組み込み関数の処理は内部で Rust によって記述された関数が呼び出されているようで、VM のコードも外部関数の呼び出しが想定されている8様子ですが、実際に呼び出す方法は未確認です。
それともないのかな?
あとがき
Numbat 流行れ。ひいてはNumbatが流行ることでほかの主流言語も型システムに単位を導入するんだ。
まじで Unit-Safety とかいって流行ったりしないかな
-
ソースコードには
variable
という単語は出てくるけどドキュメント上では定数
で共通してるし、再代入できないので存在しないものとしてよいと思います。 ↩ -
主に https://github.com/sharkdp/numbat/blob/01b2a7cb4f2068d4c1ef78a79abc34355a3412fc/numbat/src/parser.rs#L3 および https://github.com/sharkdp/numbat/blob/01b2a7cb4f2068d4c1ef78a79abc34355a3412fc/numbat/src/vm.rs#L27 ↩
-
調べた範囲だとそのようでしたが、実際には自分が気付いてないだけの可能性がありますので記述の間違いありましたらご指摘お願いいたします。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
https://github.com/sharkdp/numbat/blob/01b2a7cb4f2068d4c1ef78a79abc34355a3412fc/numbat/src/parser.rs#L9 ↩
-
https://github.com/sharkdp/numbat/blob/01b2a7cb4f2068d4c1ef78a79abc34355a3412fc/numbat/src/decorator.rs#L6 ↩
-
https://github.com/sharkdp/numbat/blob/01b2a7cb4f2068d4c1ef78a79abc34355a3412fc/numbat/src/parser.rs#L3 に反復文の記述が見当たらない ↩
-
https://github.com/sharkdp/numbat/blob/01b2a7cb4f2068d4c1ef78a79abc34355a3412fc/numbat/src/parser.rs#L8 にジェネリクスの記述が見当たらない ↩
-
https://github.com/sharkdp/numbat/blob/01b2a7cb4f2068d4c1ef78a79abc34355a3412fc/numbat/src/vm.rs#L98 ↩