新しい言語を使うときにどこまでドキュメントを読むか
◆ 高度なことをしないなら値・計算・変数・制御構文・関数くらいで割とどうにかなると思う
◆ Dictionary 系の key-value な型とクロージャーの組み合わせでクラスっぽいものを色々な言語で作ってみた
◆ Dictionary 系の key-value な型とクロージャーの組み合わせでクラスっぽいものを色々な言語で作ってみた
先に書いておくとこの記事は本題以外の内容がほとんどです
その言語をちゃんと理解して詳しくなろうとしてるなら全部読んだほうがいいでしょうけど とりあえず今作りたいものがあってそれに必要な基本的な部分だけでいいです
とりあえずは
だけあればいいかなと思います
文字列の書き方でもダブルクオートだけの言語もあったり シングルクオートでもいいけど意味が異なるとかあったりしますし トリプルクオートやプレフィックスをつけて意味が変わるものもあります
リストやマップみたいなものもリテラルでかけるものもあれば関数(コンストラクタ)を呼び出して作る言語もあります
型は数値だけでも色々ある言語もあります
スクリプト言語だとあまり違いはないですが ネイティブよりになるとバイト数で型が違いますからね
また静的型付け言語だと型をコード中に書かないといけないことも多いのでそのときの型の名前や書き方を把握しておく必要があります
基本計算は四則演算などです
文字列だったら結合したり 真偽値なら反転したりなども言語による違いがあるので見ておく必要があります
++ や += みたいな演算子は言語によってはなかったりします
変数は宣言方法や代入方法の違いなどです
宣言が不要の言語もありますし スコープが違ったり ミュータブルとイミュータブルの違いがあったりしますからね
制御構文は主に分岐と繰り返しです
繰り返しでも for-in や for-of や foreach など言語によって色々あります
else if の書き方もいくつかあります
パターンマッチングを使って分岐する言語もあります
関数は作り方と呼び出し方ですが その扱いも把握しておいたほうがいいと思います
その他の値と同じように変数に入れられるものか 変数とは扱いが異なるものなのかで使い勝手も変わります
標準入出力とかファイルの読み書きは標準ライブラリの関数の呼び出しになるでしょうし 特殊な言語でもなければけっこうどうにかなりそうです
try-catch みたいなエラー処理は入ってませんが 数百行レベルの小さなプログラムだと不要なことも多いです
ユーザー入力でのエラーなら生のエラーをコンソールに出すでも十分だったりします
モジュールやパッケージ周りも 小さいものならファイルを分割しなくても 1 ファイルに全部まとめればいいと思います
標準ライブラリのインポートという点では知っておく必要があるかもですけど
クラスみたいなものも あれば便利なところはありますが 関数だけあればどうにかなることがほとんどです
実際 最近の言語ってだいたい複数の値をまとめて返せますし クロージャー(ラムダ式)もあります
これらを使って値とそれを更新参照する関数も返せば実質クラスみたいなものですし
そんなことを思って横道にそれた結果 余計なことをしていて半日くらいが過ぎました
JavaScript の場合は簡単に書けますし むしろこの方がクラスより好みなのでよく使います
他言語だとこれほど便利ではないですが 似たようなことができるはずです
静的型付け言語でも JavaScript のオブジェクトを作ってるところに ディクショナリやテーブルやマップや連想配列を使えます
クラス的なものの構文などを理解しなくても これらでまとめて返せばどうにかなります
Dictionary 型にラムダ式も int も入れるので object 型にしてなんでも入れれるようにしています
その代わり使うところで都度キャストが必要になります
C# だと dynamic を使うこともできたのですが 静的型付け言語でということなので dynamic は使わないようしました
そう思ってやってみたのですが うまく行かなかったです
こういう感じになってできそうと思ったところで問題がでました
コメントを配置してるクロージャーのところで data を参照できないです
data は関数の返り値とするので所有権はそこで呼び出し元に渡したいです
ですが up や show の中でも data を参照するのでここは参照を保持したいです
また up では data を更新するので mutable な borrow の必要があります
しかし mutable が入ると複数の箇所に borrow できないみたいです
またクロージャー内の data のライフタイムが data と一致してないようなエラーも出ます
いろいろ試してみたのですがクロージャーでキャプチャされる場合は良い方法がないみたいです
move で所有権をクロージャーに移動することはできるようですが 2 箇所のクロージャーと共有したいですし そのままの data としても有効であってほしいです
あちこちから参照を持って自由に更新できるというのを許さない言語みたいですし できないのかもしれないです
やっぱり GC のある言語のほうが楽でいいです
GC 言語なら問題なさそうです
動的言語なので JavaScript とほぼ同じ感じで書けます
PHP もそんなに変わらないですが クロージャーで $data を参照として保持するので use を & 付きで使う必要があります
Lua は JavaScript とそんな変わらないのでこれも簡単です
C# をやったので JVM 言語もやってみようということで Kotlin でやってみました
C# と似たような感じです
Any が C# の object みたいな扱いなので Any で HashMap に入れて使うときにキャストしています
D 言語も同じく任意の型の値を入れられる Variant を使って連想配列にしています
Go では any の map です
Nim はちょっと長めになりました
というのも Nim にも Any 型はあるのですが 扱いが少し特殊で あまり推奨されないようなもののようです
なので情報も少なめでした
var で宣言した変数に対して toAny で変換するというものです
Any でラップされる形になるのですが 中身を取得する方法が関数の呼び出しで getInt などはありますが 関数みたいな特殊な型はありません
ラップできるのにアンラップできません
また proc は Any の kind が akProc のはずですが proc が外部の変数をキャプチャしてると akTuple になるようです
調べても対処方法がわからないので Union 型のような方法で実装することにしました
型定義部分を見なければ短めに書けています
静的言語を使うときは自身で定義するクラス的なものまで理解して そっちを使ったほうがいいですね
(そもそもこのクラス的なもの自体が実用性無く間違ってる気もしますが)
イミュータブルにして更新のたびにディクショナリを新しく作るという考え方なら Rust のケースでもイミュータブルで良いのでなんとかなりそうです
イミュータブルを表すため 一応 freeze させておきます
イミュータブルがデフォルトの言語ということで F# で試してみましたが問題なさそうです
動きました が main がけっこうごちゃごちゃしますね
ただ調べた感じ Zig ではクロージャーをサポートしてないようでした
匿名の構造体を使って似たことはできるらしいのですが 外側の変数はキャプチャできず 内部で作ったデータかコンパイル時に決まる値しか使えないようでした
それだと実現できなさそうなのでスキップしました
どこまでドキュメント読むべき?
普段使わない言語でちょっとしたものを作る必要があるとき 全く構文を把握してない状態で例を見て組み合わせるだけというのは少しつらいので ある程度はドキュメントの基礎やチュートリアル部分を読むわけですが 長いですよねその言語をちゃんと理解して詳しくなろうとしてるなら全部読んだほうがいいでしょうけど とりあえず今作りたいものがあってそれに必要な基本的な部分だけでいいです
とりあえずは
- 値と型
- 基本計算
- 変数
- 制御構文
- 関数
だけあればいいかなと思います
詳しく
値はリテラルの記法なども含めてです文字列の書き方でもダブルクオートだけの言語もあったり シングルクオートでもいいけど意味が異なるとかあったりしますし トリプルクオートやプレフィックスをつけて意味が変わるものもあります
リストやマップみたいなものもリテラルでかけるものもあれば関数(コンストラクタ)を呼び出して作る言語もあります
型は数値だけでも色々ある言語もあります
スクリプト言語だとあまり違いはないですが ネイティブよりになるとバイト数で型が違いますからね
また静的型付け言語だと型をコード中に書かないといけないことも多いのでそのときの型の名前や書き方を把握しておく必要があります
基本計算は四則演算などです
文字列だったら結合したり 真偽値なら反転したりなども言語による違いがあるので見ておく必要があります
++ や += みたいな演算子は言語によってはなかったりします
変数は宣言方法や代入方法の違いなどです
宣言が不要の言語もありますし スコープが違ったり ミュータブルとイミュータブルの違いがあったりしますからね
制御構文は主に分岐と繰り返しです
繰り返しでも for-in や for-of や foreach など言語によって色々あります
else if の書き方もいくつかあります
パターンマッチングを使って分岐する言語もあります
関数は作り方と呼び出し方ですが その扱いも把握しておいたほうがいいと思います
その他の値と同じように変数に入れられるものか 変数とは扱いが異なるものなのかで使い勝手も変わります
その他のもの
これだけあればとりあえずのものは作れるんじゃないかと思います標準入出力とかファイルの読み書きは標準ライブラリの関数の呼び出しになるでしょうし 特殊な言語でもなければけっこうどうにかなりそうです
try-catch みたいなエラー処理は入ってませんが 数百行レベルの小さなプログラムだと不要なことも多いです
ユーザー入力でのエラーなら生のエラーをコンソールに出すでも十分だったりします
モジュールやパッケージ周りも 小さいものならファイルを分割しなくても 1 ファイルに全部まとめればいいと思います
標準ライブラリのインポートという点では知っておく必要があるかもですけど
クラスみたいなものも あれば便利なところはありますが 関数だけあればどうにかなることがほとんどです
実際 最近の言語ってだいたい複数の値をまとめて返せますし クロージャー(ラムダ式)もあります
これらを使って値とそれを更新参照する関数も返せば実質クラスみたいなものですし
そんなことを思って横道にそれた結果 余計なことをしていて半日くらいが過ぎました
クラス的なもの
JavaScript
上で書いたクラス的なもの JavaScript だとこういうのですconst createCounter = (start) => {
const data = {}
data.count = start
data.up = () => data.count++
data.show = () => console.log(data.count)
return data
}
const counter = createCounter()
counter.show()
counter.up()
counter.show()
counter.up()
counter.show()
10
11
12
JavaScript の場合は簡単に書けますし むしろこの方がクラスより好みなのでよく使います
他言語だとこれほど便利ではないですが 似たようなことができるはずです
静的型付け言語でも JavaScript のオブジェクトを作ってるところに ディクショナリやテーブルやマップや連想配列を使えます
クラス的なものの構文などを理解しなくても これらでまとめて返せばどうにかなります
C#
静的型付け言語は C# でやってみましたDictionary<string, object> createCounter(int start)
{
var data = new Dictionary<string, object>();
data["count"] = start;
data["up"] = () =>
{
data["count"] = (int)data["count"] + 1;
};
data["show"] = () =>
{
Console.WriteLine(data["count"]);
};
return data;
}
var counter = createCounter(10);
(counter["show"] as Action)?.Invoke();
(counter["up"] as Action)?.Invoke();
(counter["show"] as Action)?.Invoke();
(counter["up"] as Action)?.Invoke();
(counter["show"] as Action)?.Invoke();
10
11
12
Dictionary 型にラムダ式も int も入れるので object 型にしてなんでも入れれるようにしています
その代わり使うところで都度キャストが必要になります
C# だと dynamic を使うこともできたのですが 静的型付け言語でということなので dynamic は使わないようしました
Rust
だいたいの言語はできそうと思いましたが Rust だとできるのでしょうかそう思ってやってみたのですが うまく行かなかったです
use std::collections::HashMap;
enum Property {
Method(Box<dyn Fn() -> ()>),
Value(i32),
}
fn main() {
let counter = create_counter(10);
if let Property::Method(func) = counter.get("show").unwrap() {
func()
}
if let Property::Method(func) = counter.get("up").unwrap() {
func()
}
if let Property::Method(func) = counter.get("show").unwrap() {
func()
}
}
fn create_counter(start: i32) -> HashMap<String, Property> {
let mut data: HashMap<String, Property> = HashMap::new();
data.insert(String::from("count"), Property::Value(start));
data.insert(String::from("up"), Property::Method(Box::new(|| {
//
})));
data.insert(String::from("show"), Property::Method(Box::new(|| {
//
})));
data
}
こういう感じになってできそうと思ったところで問題がでました
コメントを配置してるクロージャーのところで data を参照できないです
data は関数の返り値とするので所有権はそこで呼び出し元に渡したいです
ですが up や show の中でも data を参照するのでここは参照を保持したいです
また up では data を更新するので mutable な borrow の必要があります
しかし mutable が入ると複数の箇所に borrow できないみたいです
またクロージャー内の data のライフタイムが data と一致してないようなエラーも出ます
いろいろ試してみたのですがクロージャーでキャプチャされる場合は良い方法がないみたいです
move で所有権をクロージャーに移動することはできるようですが 2 箇所のクロージャーと共有したいですし そのままの data としても有効であってほしいです
あちこちから参照を持って自由に更新できるというのを許さない言語みたいですし できないのかもしれないです
やっぱり GC のある言語のほうが楽でいいです
その他の言語
このクラスっぽいものを作る方法 どの言語でもできるだろうと思ってましたが Rust の件があったので 他の言語も気になったのでいくつかの言語でやってみましたGC 言語なら問題なさそうです
Python
def create_counter(start):
data = { "count": start }
def up():
data["count"] += 1
def show():
print(data["count"])
data["up"] = up
data["show"] = show
return data
counter = create_counter(10)
counter["show"]()
counter["up"]()
counter["show"]()
counter["up"]()
counter["show"]()
動的言語なので JavaScript とほぼ同じ感じで書けます
PHP
<?php
function create_counter($start) {
$data = [];
$data['count'] = $start;
$data['up'] = function() use(&$data) {
$data['count']++;
};
$data['show'] = function() use(&$data) {
echo $data['count'], PHP_EOL;
};
return $data;
}
$counter = create_counter(10);
$counter['show']();
$counter['up']();
$counter['show']();
$counter['up']();
$counter['show']();
PHP もそんなに変わらないですが クロージャーで $data を参照として保持するので use を & 付きで使う必要があります
Lua
function create_counter(start)
local data = {}
data["count"] = start
data["up"] = function()
data["count"] = data["count"] + 1
end
data["show"] = function()
print(data["count"])
end
return data
end
local counter = create_counter(10)
counter["show"]()
counter["up"]()
counter["show"]()
counter["up"]()
counter["show"]()
Lua は JavaScript とそんな変わらないのでこれも簡単です
Kotlin
fun main() {
val counter = createCounter(10)
(counter["show"] as () -> Unit)()
(counter["up"] as () -> Unit)()
(counter["show"] as () -> Unit)()
(counter["up"] as () -> Unit)()
(counter["show"] as () -> Unit)()
}
fun createCounter(start: Int): HashMap<String, Any> {
val data = HashMap<String, Any>()
data["count"] = start
data["up"] = { data["count"] = (data["count"] as Int) + 1 }
data["show"] = { println(data.get("count")) }
return data
}
C# をやったので JVM 言語もやってみようということで Kotlin でやってみました
C# と似たような感じです
Any が C# の object みたいな扱いなので Any で HashMap に入れて使うときにキャストしています
D
import std;
void main()
{
auto counter = create_counter(10);
counter["show"].get!(void delegate())()();
counter["up"].get!(void delegate())()();
counter["show"].get!(void delegate())()();
counter["up"].get!(void delegate())()();
counter["show"].get!(void delegate())()();
}
Variant[string] create_counter(int start)
{
Variant[string] data;
void up() { data["count"] = data["count"].get!(int) + 1; }
void show() { writeln(data["count"]); }
data["count"] = start;
data["up"] = &up;
data["show"] = &show;
return data;
}
D 言語も同じく任意の型の値を入れられる Variant を使って連想配列にしています
Go
package main
import "fmt"
func main() {
counter := createCounter(10)
counter["show"].(func())()
counter["up"].(func())()
counter["show"].(func())()
counter["up"].(func())()
counter["show"].(func())()
}
func createCounter(start int) map[string]any {
data := map[string]any{}
data["count"] = start
data["up"] = func() {
data["count"] = data["count"].(int) + 1
}
data["show"] = func() {
fmt.Println(data["count"])
}
return data
}
Go では any の map です
Nim
import std/tables
type PropertyKind = enum
Value, Method
type Property = object
case kind: PropertyKind
of Value:
val: int
of Method:
fn: proc()
proc create_counter(start: int): auto =
var data = initTable[string, Property]()
proc up() =
data["count"] = Property(kind: PropertyKind.Value, val: data["count"].val + 1)
proc show() =
echo data["count"].val
data["count"] = Property(kind: PropertyKind.Value, val: start)
data["up"] = Property(kind: PropertyKind.Method, fn: up)
data["show"] = Property(kind: PropertyKind.Method, fn: show)
return data
let counter = create_counter(10)
counter["show"].fn()
counter["up"].fn()
counter["show"].fn()
counter["up"].fn()
counter["show"].fn()
Nim はちょっと長めになりました
というのも Nim にも Any 型はあるのですが 扱いが少し特殊で あまり推奨されないようなもののようです
なので情報も少なめでした
var で宣言した変数に対して toAny で変換するというものです
Any でラップされる形になるのですが 中身を取得する方法が関数の呼び出しで getInt などはありますが 関数みたいな特殊な型はありません
ラップできるのにアンラップできません
また proc は Any の kind が akProc のはずですが proc が外部の変数をキャプチャしてると akTuple になるようです
調べても対処方法がわからないので Union 型のような方法で実装することにしました
型定義部分を見なければ短めに書けています
まとめ
動的言語なら マップ的なものに全部入れてもいいけど 静的言語になるとやっぱりキャストの手間が多いです静的言語を使うときは自身で定義するクラス的なものまで理解して そっちを使ったほうがいいですね
おまけ
クラス的なものということで ミュータブルなディクショナリを更新していく前提でしたが イミュータブルがデフォルトの言語でそういう考え方で作るのが間違ってるようにも思います(そもそもこのクラス的なもの自体が実用性無く間違ってる気もしますが)
イミュータブルにして更新のたびにディクショナリを新しく作るという考え方なら Rust のケースでもイミュータブルで良いのでなんとかなりそうです
JavaScript
まず JavaScript で書くとこういう感じですイミュータブルを表すため 一応 freeze させておきます
const createCounter = (start) => {
const data = {}
data.count = start
data.up = () => createCounter(data.count + 1)
data.show = () => console.log(data.count)
return Object.freeze(data)
}
let counter = createCounter(10)
counter.show()
counter = counter.up()
counter.show()
counter = counter.up()
counter.show()
F#
let rec createCounter (start: int): Map<string, obj> =
let count = start
let up () =
createCounter(count + 1)
let show () =
printfn "%i" count
let data = Map<string, obj> [
("count", count);
("up", up);
("show", show);
]
data
let mutable counter = createCounter(10)
(counter["show"] :?> unit -> unit) ()
counter <- (counter["up"] :?> unit -> Map<string, obj>) ()
(counter["show"] :?> unit -> unit) ()
counter <- (counter["up"] :?> unit -> Map<string, obj>) ()
(counter["show"] :?> unit -> unit) ()
イミュータブルがデフォルトの言語ということで F# で試してみましたが問題なさそうです
Rust
この方針でもう一度 Rust でやってみますuse std::collections::HashMap;
enum Property {
Up(Box<dyn Fn() -> HashMap<String, Property>>),
Show(Box<dyn Fn() -> ()>),
Count(i32),
}
fn main() {
let counter = create_counter(10);
if let Property::Show(func) = counter.get("show").unwrap() {
func()
}
let counter = (if let Property::Up(func) = counter.get("up").unwrap() {
Some(func())
} else {
None
}).unwrap();
if let Property::Show(func) = counter.get("show").unwrap() {
func()
}
let counter = (if let Property::Up(func) = counter.get("up").unwrap() {
Some(func())
} else {
None
}).unwrap();
if let Property::Show(func) = counter.get("show").unwrap() {
func()
}
}
fn create_counter(start: i32) -> HashMap<String, Property> {
let count = start;
let up = move || {
create_counter(count + 1)
};
let show = move || {
println!("{}", count);
};
let data = HashMap::from([
(String::from("count"), Property::Count(count)),
(String::from("up"), Property::Up(Box::new(up))),
(String::from("show"), Property::Show(Box::new(show))),
]);
data
}
動きました が main がけっこうごちゃごちゃしますね
おまけ 2
いくつかの言語でやってみたところで 候補には一応 Zig 言語もありましたただ調べた感じ Zig ではクロージャーをサポートしてないようでした
匿名の構造体を使って似たことはできるらしいのですが 外側の変数はキャプチャできず 内部で作ったデータかコンパイル時に決まる値しか使えないようでした
それだと実現できなさそうなのでスキップしました