はじめに
色んなプログラミング言語を勉強していくと、違う言語においてある単語が似たようで実は違うものとして使われることがしばしばありますね。今回はその中で今回「シンボル」(symbol)というものに関して話します。
Symbol
というクラスはRubyでも、JavaScriptとTypeScriptでも、Dartでも存在しますが、それぞれの言語でかなり違う概念です。
実際にRubyとDartでのシンボルは大体似ているから、Ruby経験者ならDartのシンボルのことも簡単に理解できるかもしれません。
しかしJavaScriptでは全く別物です。DartがJavaScriptに似ているとよく言われていますが、シンボルに関しては全然違うので、混同してはいけないことです。
確かにDartはあまりRubyに似ていないが、シンボルの概念のことだけはRubyの影響を受けている可能性がありますね。
Dartは比較的に新しい言語で、最近スマホアプリの開発に使われるFlutterの言語として広く知られるようになったばかりで、こうやってDartとRubyを並べて比較するのはこの記事で初めてかもしれません。
なおPython、PHP、C、Fortran、IDLなど私の知っている他の言語にはシンボルというものは聞いたことないので比較の対象外です。
シンボルと呼ばれるものが存在する言語は他にもありますが、私が使ったことないから詳しく説明できないので、ここでは割愛します。
シンボルの宣言
まずはSymbolというクラスのオブジェクトの作り方です。どの言語でもオブジェクト指向言語でシンボルというのはオブジェクトの一種です。
以下シンボルオブジェクトを作成して、ある変数に入れる書き方です。
言語 | 宣言の書き方 |
---|---|
Ruby |
または
|
JavaScript と TypeScript |
|
Dart |
または
|
次はこの4つの言語におけるこのシンボルの使い方と概念について説明します。
Rubyのシンボル
Rubyではシンボルが文字列と似ています。違うのは、作る時に文字列が" "
か' '
で作られるのに対し、シンボルは:
の後ろに置くことで作られるということです。
smbl = :くるま # シンボルを作る
str = "くるま" # 文字列を作る
どれも文字を持っていて、[]
で中身の文字を参照できます。
smbl[0] # "く"
str[1] # "る"
同じ文字を持つシンボルは同じものと見做されますが、文字列とは別物扱い。
smbl==:くるま # true
smbl=="くるま" # false
また、文字列の中の文字は変更できますが、シンボルの中の文字は変更できません。
str[2] = "み"
str # "くるみ"
smbl[2] = "み" # NoMethodError: undefined method `[]=' for :くるま:Symbol
こんな似ているようで違うものが2つ存在して、使い分ける必要があって、混乱させることもありますね。これはRubyの難しさの一つとも言えます。
ただしこういう使い分けはパーフォーマンスに繋がります。シンボルは文字列ほど使い勝手がよくない一方、処理の速度が文字列より速いから。これはシンボルの存在する意味です。
文字列の方が一般的に使うが、シンボルを使う場合も多少あります。例えば実はRubyでのメソッド(関数)の名前は文字列ではなくシンボルです。
試しにクラスを作ってその中でメソッドを定義して.instance_methods
でメソッドの名前を調べたら配列の中のシンボルとして現れます。
class Kurasu
def met_a
end
def met_b
end
end
Kurasu.instance_methods(false) # [:met_a, :met_b]
なお、ここで(false)
が継承したメソッドを含まないするためですが、もしなかったらいっぱい出てきます。
[:met_a, :met_b, :instance_of?, :kind_of?, :is_a?, :tap, :public_send, :remove_instance_variable, :singleton_method, :instance_variable_set, :define_singleton_method, :method, :public_method, :extend, :to_enum, :enum_for, :<=>, :===, :=~, :!~, :eql?, :respond_to?, :freeze, :inspect, :object_id, :send, :display, :to_s, :nil?, :hash, :class, :singleton_class, :clone, :dup, :itself, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :frozen?, :methods, :singleton_methods, :protected_methods, :private_methods, :public_methods, :instance_variable_get, :instance_variables, :instance_variable_defined?, :!, :==, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__]
本当にシンボルいっぱいありますね。
その他にシンボルは普段ハッシュ(javaScriptではオブジェクト、Dartではマップと呼ぶけど)のキーに使われます。
例えばこんなハッシュを作って使ってみます。
hito = {"myouji": "Yagami", "namae": "Tsuki"}
hito.keys[0].class # Symbol
hito["myouji"] # nil
hito[:myouji] # "Yagami"
これが文字列だと勘違いして同じ文字列で参照しても何も出ません。
実際に基本的にRubyのハッシュの:
ではなく=>
が使われますが、上述の宣言は
hito = {:myouji => "Yagami", :namae => "Tsuki"}
の略です。
もし普通の文字列をキーとして使いたいなら可能ですが、その場合こう書きます。
hito = {"myouji" => "Yagami", "namae" => "Tsuki"}
hito["myouji"] # "Yagami"
hito.keys[0].class # String
JSON.parse
メソッドでjsonからハッシュに変換する時に普段キーは文字列となりますが、シンボルにして欲しい場合はsymbolize_names: true
を書きます。
require 'json'
json = '{"hoge": {"fuya": "piyo"}}'
JSON.parse(json) # {"hoge"=>{"fuya"=>"piyo"}}
JSON.parse(json, symbolize_names: true) # {:hoge=>{:fuya=>"piyo"}}
普段文字列とシンボルは別のものとして扱われますが、どっちを使ってもいい場合もあります。
例えばRuby on Railsのコントローラのparamsです。普段はシンボルですが、文字列で参照しても問題ないです。なぜなら実質的にこのparamsはハッシュではなく、中身には文字列をシンボルと同一視させるという仕組みが含まれているから。普段のハッシュと混同してはいけません。
これについて詳しくはこの記事:
このようにRubyでは色んなところでシンボルが活躍しています。
JavaScriptのシンボル
JavaScriptでは元々Symbolというクラスは存在しませんでした。このクラスが追加されたのはES (ECMAScript 2015)からです。だから出番は少なくて、文字列と混同することはあまりないでしょう。
JavaScriptのシンボルはRubyと全然違うもので、使い道も少ないです。使わなければならないという場合もないので、無理に使おうとしなくてもあまり問題ないでしょう。
JavaScriptでのシンボルはざっくり言うと「唯一無二の何か」を作るためです。同じようなシンボルを作っても同じものだと見做されません。
let smbl1 = Symbol("シンボル")
let smbl2 = Symbol("シンボル")
alert(smbl1==smbl2) // false
alert(smbl1===smbl2) // false
alert(smbl1=="シンボル") // false
alert(smbl1==smbl1) // true
alert(smbl1===smbl1) // true
ただし直接その変数を代入したら流石に同じものだと認められますね。
let smbl3 = smbl1
alert(smbl1==smbl3) // true
alert(smbl1===smbl3) // true
そんなシンボルの使い道は2つあります。まず何かの目的のために区別するオブジェクトとして使うのです。
例えばこんな分岐のある関数で使うです。
const UDON = Symbol()
const SOBA = Symbol()
const RAAMEN = Symbol()
function chuumon(k) {
if (k === UDON) {
alert("うどんを注文します")
}
else if (k === SOBA) {
alert("そばを注文します")
}
else {
alert("らーめんを注文します")
}
}
chuumon(UDON) // うどんを注文します
chuumon(SOBA) // そばを注文します
chuumon(RAAMEN) // らーめんを注文します
実際にはわざわざシンボルを使わなくてもただ違う数字など使ってもいいはずですね。
const UDON = 0
const SOBA = 1
const RAAMEN = 2
ただしこの場合なら使う時にわざわざ変数を使わくなても単に数字を入れたらいいですね。
chuumon(0) // うどんを注文します
chuumon(1) // そばを注文します
chuumon(2) // らーめんを注文します
実際にPythonなどシンボルの概念がない言語ではそうです。ここでシンボルを使うことでこの変数を使わなければならないということになります。
わざわざ数字ではなく変数を使うのは冗長に見えるかもしれませんが、これは可読性のためでもあるから、そう考えるとシンボルは役に立ちます。
もう一つの用途は、あるクラスのプライベートなプロパティを作るためです。
例えばこんなクラスを作ります。
let OMOSA = Symbol();
let TAKASA = Symbol();
class Pokemon {
constructor(namae, omosa, takasa){
this.namae = namae;
this[OMOSA] = omosa;
this[TAKASA] = takasa;
}
jouhou(){
alert(this.namae +"、重さ:"+this[OMOSA] + "、高さ:"+this[TAKASA]);
}
}
export {Pokemon}
ここで重さと高さのプロパティはシンボルが使われますね。しかしシンボルはこのファイルの中で定義されて、exportされないので、外でこのクラスを使う時にこのプロパティにどうしようするすべはありません。
鍵を持っていなくて勝手に作ることもできないから入室できないということです。こうやって実質的にプライベートなプロパティとなります。
このクラスの使う例:
import {Pokemon} from "./pkcls.js";
let kamonegi = new Pokemon("カモネギ", 15, 0.8);
kamonegi.jouhou(); // カモネギ、重さ:15、高さ:0.8
alert(kamonegi.namae); // カモネギ
こうやってメソッドを通じて重さと高さの情報は表示できますが、直接に重さと高さの値にアクセスすることはできません。ここで名前は普通のプロパティなので普通にアクセスできますが。
こんな風に、JavaScriptのシンボルはRubyのシンボルとは全く別物です。
TypeScriptのシンボル
知っている人が多いと思いますが、実はTypeScriptはJavaScriptと殆ど似ていて、シンボルの概念もほぼJavaScriptと同じなのでここでは省略します。
Dartのシンボル
Dartが生まれたのは2011年で、あの時JavaScriptにシンボルの機能(2015年から)はまだ正式に搭載されていないので、JavaScriptの影響をたくさん受けたもののDartのシンボルはJavaScriptのシンボルとは関係ないはずです。寧ろRubyのシンボルと似ています。
Dartではシンボルの概念はRubyと似て、「文字列と似ている何か」のことです。ただし文字列みたいに[ ]
で文字を参照することはできません。
void main() {
var smbl = #kuma;
var str = "kuma";
print(str); // kuma
print(smbl); // Symbol("kuma")
print(smbl == str); // false
print(smbl == #kuma); // true
print(str[1]); // u
print(smbl[1]); // Error: The operator '[]' isn't defined for the class 'Symbol'.
}
文字列と同じく、普段どんな文字も使えますが、#
で書く方法はラテン文字や数字しか使えないので、日本語などを使いたい場合Symbol()
で作ります。
var smbl = Symbol("シンボル");
Rubyと同じように、実はDartの色んなところはシンボルによって成されています。例えば関数名、クラス名、変数名、全部ただの文字列ではなく、シンボルです。
それを見せるためにmirrors
という組み込みライブラリのcurrentMirrorSystem()
関数を使って全てこの場で宣言されたものの名前を表示させます。
import 'dart:mirrors';
void kansuu() {}
class kurasu {}
int? hensuu;
void main() {
currentMirrorSystem().isolate.rootLibrary.declarations.forEach((k, v) {
print('$k: $v');
});
}
Symbol("kurasu"): ClassMirror on 'kurasu'
Symbol("hensuu"): VariableMirror on 'hensuu'
Symbol("kansuu"): MethodMirror on 'kansuu'
Symbol("main"): MethodMirror on 'main'
このように、宣言された関数(mainも含め)とクラスと変数の名前はシンボルで表示されます。
ただしRubyと違って、普段意識して使う場合は殆どないです。特にFlutterを使う時にシンボルに触れる場面はあまりないでしょう。だからググってもDartのシンボルに関する情報は少ないです。
シンボルの存在を知らなくても殆どの場合は普通にFlutterを使ってアプリを開発できるはずです。
Dartではシンボルがマップ(Rubyでのハッシュ、JavaScriptでのオブジェクト、Pythonでの辞書)のキーに使うこともできます。
シンボルをマップに使う必要がある場面は例えば、キーワード(名前付き引数)を持つ関数をFunction.apply
で呼び出す時です。
例えばこんな関数があります。
void shoukai(shainA, shainB, {kaisha, shokui}) {
print('$shainAと$shainBは$kaishaの$shokuiです');
}
ここではkaishaとshokuiが{ }
に囲まれているので、前のshainAとshainBみたいな位置引数ではなく、キーワードです。
使う時に普段はこう書きます。
void main() {
shoukai(
'松井K',
'越水R',
kaisha: '株式会社XXX',
shokui: 'エンジニア',
);
}
松井Kと越水Rは株式会社XXXのエンジニアです
ただし、予め引数を持つリストとキーワードを持つマップを準備してFunction.apply
で呼び出すという方法もあります。その場合のマップのキーワードは文字列ではなく、シンボルです。
void main() {
var argLis = ['松井K', '越水R'];
var keyMap = {
#kaisha: '株式会社XXX',
#shokui: 'エンジニア',
};
Function.apply(shoukai, argLis, keyMap);
}
つまり関数キーワードの名前もシンボルです。
このようにシンボルと文字列の違いを意識して使う場合もありますが、Rubyと比べて出番が少なめです。
終わりに
こんな感じで、以上4つの言語におけるシンボルの違いについて説明しました。
ちなみに似ているようで実は違うものは言語学で「空似言葉」(false friend)と呼ばれます。普段は自然言語において使われる概念ですが、プログラミング言語でも同じようなことがあるようですね。
逆に余談ですが、同じようなものなのに違う言語で違う呼び方をする場合もありますね。例えばこれです。
言語 | 名称 | |
---|---|---|
Python | 辞書 | Dictionary |
PHP | 連想配列 | Associative Array |
Ruby | ハッシュ | Hash |
JavaScript TypeScript |
オブジェクト | Object |
Dart | マップ | Map |
一般的に言えばどう呼べばいいでしょう?統一したらいいですね。
このように混乱を招くことが多いですが、それでも色んな言語の違いを比べることもやり甲斐があって楽しいことだと思いますね。