デジスマチームの荒谷(@_a_akira)です。
最近、社内で各言語のQuineを作成するブームが起きています。この流れに乗って、私もDartでQuineを作成してみましたので、本記事でその仕組みを解説します。
エムスリーは2025年9月10日(水)から12日(金)に開催されるDroidKaigi 2025にゴールドスポンサー、また2025年11月13日(木)に開催のFlutter Kaigi 2025にはシルバースポンサーとして協賛します。 イベントでは、今回作成したQuineをデザインしたクリアファイルをノベルティとして配布予定です。ぜひ弊社ブースにお立ち寄りいただき、お手に取ってみてください!

Quineとは
Quineとは自身のソースコードと同じ文字列を出力するプログラムのことです。社内ではすでに様々な言語?でQuineが作成されています。
DartのQuine
そして、今回私が作成したDartのQuineがこちらです。
import'dart:convert';void main(){var(w,e,l,c,o,m,E)=((joinM3)=>joinM3.group(0),(s,f)=>base64.decode(
f(s)),(s)=>s.split(':'),(s)=>s.replaceAll(RegExp(r'\s'),''),(b)=>List.generate(8,(l)=>(b>>(7-l))&1),
"""aW1wb3J0J2RhcnQ6Y29udmVydCc7dm9pZCBtYWluKCl7dmFyKHcsZSxsLGMsbyxtLEUpPSgoam9pbk0 zKT0+am9pbk0zL
mdyb3VwKDApL ChzLGYpPT5iYXNlNjQuZGV jb2RlKApmKHMpKSwo cyk9PnMuc3B
saXQoJzonKSwoc y k9PnMucmVwbGFjZUFsbCh SZWdFeHAocidccycp LCcnKSwoY
ik9Pkxpc3QuZ2VuZX JhdGUoOCwobCk9PihiP j4oNy1sKSkmMSksCiIi IiVzIiIi
LDAKKTtwcmludChSZ WdFeHAoJy57MSwxM DB9JykuYWxsTWF0Y2hl cyhlKGwob SlbMV0sY
ykuZXhwYW5kKG8pLm 1hcCgoZik9PmY9P TA/KCgpe3ZhciB1PXV0ZjguZGVjb2R lKGUobAo
obSlbMF0sYykpLnJl cGxhY2VBbGwoJ 1xyXG4nLCcnKS5yZXBsYWNlRmlyc3Qo JyVzJyxj
KG0pKTtyZXR1cm4gR Tx1Lmxlbmd0a D91W0UrK106Jyc7fSkoKTonICcpLm pvaW4oCik
pLm1hcCh3KS5qb2l u KCdcbicpKT t9 IC8vID09PT09PT0gV2UgYXJlIGh pcmluZyEgPT
09PT09PSBXZWxjb21 lI SA9PT09PT 09I EpvaW4gTTMgZ3JvdXAgPT0 9PT09PT0gL
y8KLy8gPT09PT09P T09P T09PT09 PSB GaW5kIHRoaXMgUXVpbmU gYXQ6IGh
0dHBzOi8vd3d3Lm0 zdGVj aC5i bG9n L2VudHJ5L2RhcnQtcXVp bmUgPT 09PT09
PT09PT09PT09PSAv Lwo=:A AA AAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAA
AAPAAAAP/4AAAf/w AAf/gAA A L/gAAD/ 8AAH//gAAAH/AAAf8AAB///AAAAf/AA D/wAAH
AH8AAAB/8AAf/gA AAAfwAA AH/4AD/ 8AAAAB/AAAAf/wAP/4AAAAP4AAAD7/ gB5/g
AAAD+AAAAHn+APH +AAAH/8A AAA8P8B8 f4AAB//8AAADwf8Ph/wAADA/8AAAPA /58H+A
AAAA/wAAA8B/vgP 8AAAAB/AA AHwH/8B/wA AAAP+AAAfAP/gH/AA AAA/wAAB8Af 8AP8AAG
AD+AAAHgA/gA/wA B4AfwAAN/sB +Av/2AH//8A AB//wDwD//4AH/+ AAAAAAAAAA AAAAAAAA
AAAAAAAAAA A A AAAAAAA AAAAAAA A A AAAAAAAAAA AAAAAAAAAA
AAAAAAAAA AAAAAAAA AAAAAAAA AAAAAAAAAAAA AAAAAAAA""",0
);print(RegExp('.{1,100}').allMatches(e(l(m)[1],c).expand(o).map((f)=>f==0?((){var u=utf8.decode(e(l
(m)[0],c)).replaceAll('\r\n','').replaceFirst('%s',c(m));return E<u.length?u[E++]:'';})():' ').join(
)).map(w).join('\n'));} // ======= We are hiring! ======= Welcome! ======= Join M3 group ======== //
// ================ Find this Quine at: https://www.m3tech.blog/entry/dart-quine ================ //
変数名やgroup関数を活かして、Welcome join M3 group というメッセージが隠れているのがおしゃれですね。
解説
基本的な仕組みは、先に紹介したKotlinやSwiftのQuineと同様です。
ソースコード全体から、データ部分(長い文字列)を %s に置き換えたテンプレートを作成します。そして、そのテンプレートと、M3のアスキーアート(AA)を0と1で表現したビットデータを : で結合し、Base64でエンコードします。
実行時には、このエンコードされたデータをデコードし、テンプレートの %s の部分にデータ自身を埋め込むことで、元のソースコードを復元して出力するという仕組みです。
このままでは読みにくいため、まずは分かりやすいようにフォーマットしてみましょう。
import 'dart:convert'; void main() { var (w, e, l, c, o, m, E) = ( (joinM3) => joinM3.group(0), (s, f) => base64.decode(f(s)), (s) => s.split(':'), (s) => s.replaceAll(RegExp(r'\s'), ''), (b) => List.generate(8, (l) => (b >> (7 - l)) & 1), """aW1wb3J0J2RhcnQ6Y29udmVydCc7dm9pZCBtYWluKCl7dmFyKHcsZSxsLGMsbyxtLEUpPSgoam9pbk0 zKT0+am9pbk0zL mdyb3VwKDApL ChzLGYpPT5iYXNlNjQuZGV jb2RlKApmKHMpKSwo cyk9PnMuc3B saXQoJzonKSwoc y k9PnMucmVwbGFjZUFsbCh SZWdFeHAocidccycp LCcnKSwoY ik9Pkxpc3QuZ2VuZX JhdGUoOCwobCk9PihiP j4oNy1sKSkmMSksCiIi IiVzIiIi LDAKKTtwcmludChSZ WdFeHAoJy57MSwxM DB9JykuYWxsTWF0Y2hl cyhlKGwob SlbMV0sY ykuZXhwYW5kKG8pLm 1hcCgoZik9PmY9P TA/KCgpe3ZhciB1PXV0ZjguZGVjb2R lKGUobAo obSlbMF0sYykpLnJl cGxhY2VBbGwoJ 1xyXG4nLCcnKS5yZXBsYWNlRmlyc3Qo JyVzJyxj KG0pKTtyZXR1cm4gR Tx1Lmxlbmd0a D91W0UrK106Jyc7fSkoKTonICcpLm pvaW4oCik pLm1hcCh3KS5qb2l u KCdcbicpKT t9 IC8vID09PT09PT0gV2UgYXJlIGh pcmluZyEgPT 09PT09PSBXZWxjb21 lI SA9PT09PT 09I EpvaW4gTTMgZ3JvdXAgPT0 9PT09PT0gL y8KLy8gPT09PT09P T09P T09PT09 PSB GaW5kIHRoaXMgUXVpbmU gYXQ6IGh 0dHBzOi8vd3d3Lm0 zdGVj aC5i bG9n L2VudHJ5L2RhcnQtcXVp bmUgPT 09PT09 PT09PT09PT09PSAv Lwo=:A AA AAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAA AAPAAAAP/4AAAf/w AAf/gAA A L/gAAD/ 8AAH//gAAAH/AAAf8AAB///AAAAf/AA D/wAAH AH8AAAB/8AAf/gA AAAfwAA AH/4AD/ 8AAAAB/AAAAf/wAP/4AAAAP4AAAD7/ gB5/g AAAD+AAAAHn+APH +AAAH/8A AAA8P8B8 f4AAB//8AAADwf8Ph/wAADA/8AAAPA /58H+A AAAA/wAAA8B/vgP 8AAAAB/AA AHwH/8B/wA AAAP+AAAfAP/gH/AA AAA/wAAB8Af 8AP8AAG AD+AAAHgA/gA/wA B4AfwAAN/sB +Av/2AH//8A AB//wDwD//4AH/+ AAAAAAAAAA AAAAAAAA AAAAAAAAAA A A AAAAAAA AAAAAAA A A AAAAAAAAAA AAAAAAAAAA AAAAAAAAA AAAAAAAA AAAAAAAA AAAAAAAAAAAA AAAAAAAA""", 0 ); print(RegExp('.{1,100}') .allMatches(e(l(m)[1], c) .expand(o) .map((f) => f == 0 ? (() { var u = utf8.decode(e(l(m)[0], c)).replaceAll('\r\n', '').replaceFirst('%s', c(m)); return E < u.length ? u[E++] : ''; })() : ' ') .join()) .map(w) .join('\n')); } // ======= We are hiring! ======= Welcome! ======= Join M3 group ======== // // ================ Find this Quine at: https://www.m3tech.blog/entry/dart-quine ================ //
フォーマットしただけでも、少し処理の流れが見えてきました。 main関数は大きく分けて「変数宣言」と「printによる出力」の2つの部分で構成されています。
宣言部の詳細
このQuineは、AAを埋め込みつつ、100文字ごとに改行するために、変数名を1文字にしたり、型定義を省略したりといった工夫をしています。
各変数がどのような役割を持っているか、分かりやすい名前に書き換えてみましょう。
import 'dart:convert'; import 'dart:typed_data'; /// w → extractMatch - 正規表現マッチから文字列を抽出する関数 /// e → base64Decode - Base64デコードを行う関数 /// l → splitOnColon - 文字列をコロンで分割する関数 /// c → removeWhitespace - 空白文字を除去する関数 /// o → byteToBits - Byteをbit配列に変換する関数 /// m → encodedData - Base64エンコードされたデータ /// E → charIndex - 文字のインデックスカウンター void main() { // 1. 正規表現マッチから文字列を抽出する関数 String? extractMatch(RegExpMatch? match) => match?.group(0); // 2. Base64デコードを行う関数 Uint8List base64Decode(String source, String Function(String) formatter) => base64.decode(formatter(source)); // 3. 文字列をコロンで分割する関数 List<String> splitOnColon(String source) => source.split(':'); // 4. 空白文字を除去する関数 String removeWhitespace(String source) => source.replaceAll(RegExp(r'\s'), ''); // 5. Byteをbit配列に変換する関数 List<int> byteToBits(int byte) => List.generate(8, (int bitIndex) => (byte >> (7 - bitIndex)) & 1); // 6. Base64エンコードされたデータ(プログラムのソースとAAのデータ) String encodedData = """aW1wb3J0J2RhcnQ6Y29udmVydCc7dm9pZCBtYWluKCl7dmFyKHcsZSxsLGMsbyxtLEUpPSgoam9pbk0 zKT0+am9pbk0zL mdyb3VwKDApL ChzLGYpPT5iYXNlNjQuZGV jb2RlKApmKHMpKSwo cyk9PnMuc3B saXQoJzonKSwoc y k9PnMucmVwbGFjZUFsbCh SZWdFeHAocidccycp LCcnKSwoY ik9Pkxpc3QuZ2VuZX JhdGUoOCwobCk9PihiP j4oNy1sKSkmMSksCiIi IiVzIiIi LDAKKTtwcmludChSZ WdFeHAoJy57MSwxM DB9JykuYWxsTWF0Y2hl cyhlKGwob SlbMV0sY ykuZXhwYW5kKG8pLm 1hcCgoZik9PmY9P TA/KCgpe3ZhciB1PXV0ZjguZGVjb2R lKGUobAo obSlbMF0sYykpLnJl cGxhY2VBbGwoJ 1xyXG4nLCcnKS5yZXBsYWNlRmlyc3Qo JyVzJyxj KG0pKTtyZXR1cm4gR Tx1Lmxlbmd0a D91W0UrK106Jyc7fSkoKTonICcpLm pvaW4oCik pLm1hcCh3KS5qb2l u KCdcbicpKT t9 IC8vID09PT09PT0gV2UgYXJlIGh pcmluZyEgPT 09PT09PSBXZWxjb21 lI SA9PT09PT 09I EpvaW4gTTMgZ3JvdXAgPT0 9PT09PT0gL y8KLy8gPT09PT09P T09P T09PT09 PSB GaW5kIHRoaXMgUXVpbmU gYXQ6IGh 0dHBzOi8vd3d3Lm0 zdGVj aC5i bG9n L2VudHJ5L2RhcnQtcXVp bmUgPT 09PT09 PT09PT09PT09PSAv Lwo=:A AA AAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAA AAPAAAAP/4AAAf/w AAf/gAA A L/gAAD/ 8AAH//gAAAH/AAAf8AAB///AAAAf/AA D/wAAH AH8AAAB/8AAf/gA AAAfwAA AH/4AD/ 8AAAAB/AAAAf/wAP/4AAAAP4AAAD7/ gB5/g AAAD+AAAAHn+APH +AAAH/8A AAA8P8B8 f4AAB//8AAADwf8Ph/wAADA/8AAAPA /58H+A AAAA/wAAA8B/vgP 8AAAAB/AA AHwH/8B/wA AAAP+AAAfAP/gH/AA AAA/wAAB8Af 8AP8AAG AD+AAAHgA/gA/wA B4AfwAAN/sB +Av/2AH//8A AB//wDwD//4AH/+ AAAAAAAAAA AAAAAAAA AAAAAAAAAA A A AAAAAAA AAAAAAA A A AAAAAAAAAA AAAAAAAAAA AAAAAAAAA AAAAAAAA AAAAAAAA AAAAAAAAAAAA AAAAAAAA"""; // 文字のindex counter int charIndex = 0; print(RegExp('.{1,100}') .allMatches(base64Decode(splitOnColon(encodedData)[1], removeWhitespace) .expand(byteToBits) .map((f) => f == 0 ? (() { var u = utf8 .decode(base64Decode(splitOnColon(encodedData)[0], removeWhitespace)) .replaceAll('\r\n', '') .replaceFirst('%s', removeWhitespace(encodedData)); return charIndex < u.length ? u[charIndex++] : ''; })() : ' ') .join()) .map(extractMatch) .join('\n')); }
それぞれの関数の役割を解説します。
1. extractMatch (元の w )
正規表現の Match オブジェクトから、マッチした部分の文字列を抽出します。
2. base64Decode (元の e)
Base64エンコードされた文字列をデコードします。 実際のコードは同じformatterを使っていますが、宣言を同時に行っている都合で、formatterは関数を渡す形になっています。
3. splitOnColon (元のl)
ソースコードのテンプレートとAAのデータを分離するために、文字列を : で分割します。
4. removeWhitespace (元のc)
Base64文字列に含まれる空白文字を正規表現で除去します。 こちらはいろいろな書き方があると思いますが、文字数調整のためこのようにしました。
5. byteToBits (元のo)
Uint8List の各バイトを8桁のビット(0か1)のリストに変換します。これはAAを表現するための処理です。 この処理は、以下のようにバイトを2進数文字列に変換する方法でも実現できますが、今回は文字数削減のためにビット演算を用いています。
var binaryString = aaBytes .map((byte) => byte.toRadixString(2).padLeft(8, '0')) .join(); String result = binaryString .split('') .map((bit) => bit == '0' ? (charIndex < restoredSource.length ? restoredSource[charIndex++] : '') : ' ') .join();
print処理の詳細
次に出力部分を分解してみましょう。
// A. 文字列の分離 List<String> parts = splitOnColon(encodedData); String sourceData = parts[0]; // プログラムのソースコード部分 String aaData = parts[1]; // AAデータ部分 // B. AAから文字列を生成 Uint8List aaBytes = base64Decode(aaData, removeWhitespace); Iterable<int> bits = aaBytes.expand(byteToBits); // C. プログラムソースを復元 String restoredSource = utf8 .decode(base64Decode(sourceData, removeWhitespace)) .replaceAll('\r\n', '') .replaceFirst('%s', removeWhitespace(encodedData)); // D. ビットパターンからAAになるようにmapping String result = bits .map((int bit) => bit == 0 ? (charIndex < restoredSource.length ? restoredSource[charIndex++] : '') : ' ') .join(); // E. 100文字毎に改行して出力 print(RegExp('.{1,100}') .allMatches(result) .map(extractMatch) .join('\n'));
A. データの分離
まず、エンコードされた文字列を : で分割し、ソースコードのテンプレートとAAのデータに分けます。
List<String> parts = splitOnColon(encodedData); String sourceData = parts[0]; // プログラムのソースコード部分 String aaData = parts[1]; // AAデータ部分
B. AAのビット化
AAのデータをデコードし、byteToBits 関数を使って0と1のビットリストに変換します。
Uint8List aaBytes = base64Decode(aaData, removeWhitespace);
Iterable<int> bits = aaBytes.expand(byteToBits);
C. ソースコードの復元
ソース部分も空白と改行を削除してデコードし、冒頭に解説した通り、String部分をエンコードしたソースコードとAAの組み合わせに置き換えます。
String restoredSource = utf8 .decode(base64Decode(sourceData, removeWhitespace)) .replaceAll('\r\n', '') .replaceFirst('%s', removeWhitespace(encodedData));
D. AAへのマッピング
ビットリストを走査し、ビットが0の部分に復元したソースコードの文字を1文字ずつ配置し、ビットが1の部分には空白を配置します。これにより、AAが描画されます。 本来は文字数がピッタリ収まれば、lengthの判定は不要ですが、Quineの文字数調整のためにAA部分に余計な0を追加しているのでindex out of boundsの判定をしています。 ここは工夫の余地がありそうです。
String result = bits .map((int bit) => bit == 0 ? (charIndex < restoredSource.length ? restoredSource[charIndex++] : '') : ' ') .join();
E. 整形して出力
最後に、生成された文字列を正規表現で100文字ごとに区切り、改行を挟んで出力します。これでQuineの完成です。
print(RegExp('.{1,100}') .allMatches(result) .map(extractMatch) .join('\n'));
まとめ
一見複雑に見えるQuineも、細かく分解してみると意外とシンプルな処理の組み合わせでできていることがお分かりいただけたかと思います。 皆さんもぜひ、お気に入りの言語でQuine作りに挑戦してみてはいかがでしょうか。「Dartでもっと面白いQuineが書ける!」といったアイデアも大歓迎です!
今回ご紹介したDart版Quineのクリアファイルを、DroidKaigi 2025とFlutterKaigi 2025の弊社ブースで配布しますので、ぜひお立ち寄りください!
We are Hiring!
エムスリーでは、M3デジカルスマート診察券をはじめ、現時点で7つのアプリでFlutterが採用されています! Flutterに限らず、プログラミングが大好きなギークなエンジニアを募集しています!ご興味のある方は、ぜひ下記リンクからご応募ください。