◆ 高度なことをしないなら値・計算・変数・制御構文・関数くらいで割とどうにかなると思う
◆ Dictionary 系の key-value な型とクロージャーの組み合わせでクラスっぽいものを色々な言語で作ってみた

先に書いておくとこの記事は本題以外の内容がほとんどです

どこまでドキュメント読むべき?

普段使わない言語でちょっとしたものを作る必要があるとき 全く構文を把握してない状態で例を見て組み合わせるだけというのは少しつらいので ある程度はドキュメントの基礎やチュートリアル部分を読むわけですが 長いですよね
その言語をちゃんと理解して詳しくなろうとしてるなら全部読んだほうがいいでしょうけど とりあえず今作りたいものがあってそれに必要な基本的な部分だけでいいです

とりあえずは

  • 値と型
  • 基本計算
  • 変数
  • 制御構文
  • 関数

だけあればいいかなと思います

詳しく

値はリテラルの記法なども含めてです
文字列の書き方でもダブルクオートだけの言語もあったり シングルクオートでもいいけど意味が異なるとかあったりしますし トリプルクオートやプレフィックスをつけて意味が変わるものもあります
リストやマップみたいなものもリテラルでかけるものもあれば関数(コンストラクタ)を呼び出して作る言語もあります

型は数値だけでも色々ある言語もあります
スクリプト言語だとあまり違いはないですが ネイティブよりになるとバイト数で型が違いますからね
また静的型付け言語だと型をコード中に書かないといけないことも多いのでそのときの型の名前や書き方を把握しておく必要があります

基本計算は四則演算などです
文字列だったら結合したり 真偽値なら反転したりなども言語による違いがあるので見ておく必要があります
++ や += みたいな演算子は言語によってはなかったりします

変数は宣言方法や代入方法の違いなどです
宣言が不要の言語もありますし スコープが違ったり ミュータブルとイミュータブルの違いがあったりしますからね

制御構文は主に分岐と繰り返しです
繰り返しでも 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 ではクロージャーをサポートしてないようでした
匿名の構造体を使って似たことはできるらしいのですが 外側の変数はキャプチャできず 内部で作ったデータかコンパイル時に決まる値しか使えないようでした
それだと実現できなさそうなのでスキップしました