エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

OCamlでQuineを作った

ギークな皆さん、こんにちは、こんばんは。

10月より基盤開発チームのチームリーダーもやりはじめました、エムスリーエンジニアリンググループVPoEの河合(@vaaaaanquish)です。

夏の思い出。本文とは関係ありません。

当記事では、私がOCamlに初挑戦した話、そしてQuineを作った話を書いていきます。

基盤チームとOCaml

改めまして、基盤チームのチームリーダーの河合(@vaaaaanquish)です。 10月より、インフラ話とお酒が好きな@yokomotodと2人チームリーダー体制でやっております。 基盤チームは、エムスリー、ひいては医療を支える基盤となるサービスを横断的に開発するチームです。

横断型のチームですので、様々な技術を抱えており、認証系のサービスから大規模メルマガ配信基盤、言語もPython、RubyからScalaまで幅広く対応しています。

Scalaといえば、関数型言語。基盤チームには関数型言語が大好きなメンバーが在籍しておりまして、今年の関数型祭りへのスポンサードを薦めてくれたり、Software Designで関数型言語で連載を書いたり、社内で行っているLTでさえ毎週のようにOCamlの話をしてくれます。基盤チームのサービスにはまだOCamlは導入されていませんが、ここでOCamlを学ばない手はないと思い学習を始めました。

Quineを作った

というわけで、OCamlでQuineを書きました。

#use"topfind"#require"zstd"#require"base64"#require"compiler-libs";;open Base64;;(*@m3_engineering*)
open Toploop;;let r=decode_exn and n x y z=Str.global_replace (Str.regexp x) y z;;(*We're hiring!!*)
open Buffer;;execute_phrase true Format.std_formatter (!parse_toplevel_phrase (Lexing.from_string (n
"#" " " (n "[ \n]" "" {|let#()=let#b=r#"I3VzZSJ0b3BmaW5kIiNyZXF1aXJlInpzdGQiI3Jl    cXVpcmUiYmFzZTY0
IiNyZXF1aX             JlImNvbXBpbGVyLWxpYnMi            OztvcGVuIEJhc2U2ND            s7KCpAbTNfZW5
naW5lZXJpbmc           qKW9wZW4gVG9wbG9vcDs7          bGV0IHI9ZGVjb2RlX2V                4biBhbmQgbi
B4IHkgej1TdHIuZ         2xvYmFsX3JlcGxhY2Ug         KFN0ci5yZWdleHAgeCk     geSB6O        zsoKldlJ3J
lIGhpcmluZyEhKi           lvcGVuIEJ1ZmZlcj          s7ZXhlY3V0ZV9waHJhc   2UgdHJ1ZS       BGb3JtYXQu
c3RkX2Zvcm1hdHR           lciAoIXBhcnNlX3           RvcGxldmVsX3BocmFzZSAoTGV4aW5nL       mZyb21fc3R
yaW5nIChuIiMiIC            IgIiAobiAiWyB            cbl0iICIiIHt8bGV0IygpPWxldCNiPX      IjIiVzIiNpb
iNsZXQjYWRjPWFk             ZF9jaGFyI2lu            I2xldCNnbT1uIygiJSJeInMiKSMoZW      5jb2RlX2V4bi
NiKSNiI2luI2xld    C         N0PVpzdGQu    ZG       Vjb21wcmVzcyMyNjI1IyhyIyJL        TFV2L1dCQkNkME
VBQkpGQlFYZ0Q0M    Hh        BMzEvOVZw    cjl        XdDFmTFZkN1ZYcnAwK2d3UE            prV0FNZ1MwR3
IxaXNXcWZSVkFB    TEhk        b0tnN2t     Ba0        9CNml1VmZ5R25ORkhvK0                 JKdWhNc2JW
UWlQNmdwOGZCRl    FpR2N         pMFl     BVWJ        4UGR4Z3Z5OTB4QkNoSkVYa1ZjZ2Ju         OE9QNFBNa
zRqbFlSOHE4REo    2RHMyZ         Wl     CNVFU        YmtDU29Ca2dVVHNaVnY1eW5QSVFEMmE        2aE51SzU
wQUJBakZTd2lHR    kw5RHFJ        R     lVpIik        jaW4jcHJpbnRfZW5kbGluZShsZXQjYnV       mPWNyZWF
0ZSMoMjYyNSkj    aW4jbGV0             I2o9cmVm        IzAjaW4jU3RyaW5nLml0ZXIoZnVuY3R       pb258JzE
nLT5hZGMjYnVm    IycjJyN8J           1xuJy0+YW        RjI2J1ZiMnXG4nI3xfLT5hZGMjYnVm        I2dtLlsh
al07I2luY3Ija    ikjdDsjY29         udGVudHMjY        nVmKTs7fH0pKSkp"   #in#let#adc       =add_char
#in#let#gm=n#    ("%"^"s")#(       encode_exn#        b)#b#in#let#t=Z    std.decomp       ress#2625#
(r#"KLUv              /WBBCd0      EABJFBQ                 XgD40xA31/                   9Vpr9Wt1fLVd
7VXrp0+g              wPJkWAMg    S0Gr1isW                 qfRVAALHdoKg              7kAkOB6iuVfyGnN
FHo+BJuhMsbVQiP6gp8fBFQiGci0YAUbxPdxgvy90xBChJEXkVcgbn8OP4PMk4jlYR8q8DJ6Ds2eiB5QTbkCSoBkgUTsZVv5ynPI
QD2a6hNuK50ABAjFSwiGFL9DqIFUi")#in#print_endline(let#buf=create#(2625)#in#let#j=ref#0#in#String.iter
(function|'1'->adc#buf#'#'#|'\n'->adc#buf#'\n'#|_->adc#buf#gm.[!j];#incr#j)#t;#contents#buf);;|}))))

Quine(クワイン)は、「プログラムの出力が、ソースコード自身と完全に同じ文字列になる」ようなプログラムの事を指します。 その歴史は長く、プログラミング言語が広く普及し始めた1980年代から存在が確認されています。

簡単なQuineでは、様々な言語で以下のようなQuineが考えられます。お手元の環境で実行してみて貰えれば、全く同じコードが出力されてくる実感が湧くかなと思います。

# Pythonで簡易Quine
s='s=%r;print(s%%s)';print(s%s)

# Rubyで簡易Quine
s="s=%p;puts s%%s";puts s%s

# OCamlで簡易Quine
(fun s -> Printf.printf "%s %S" s s) "(fun s -> Printf.printf \"%s %S\" s s)"

エムスリーエンジニアリンググループはそういったギークコンテンツ好きな人が多く、社内だけでもどうやら7言語分のオリジナルQuineが存在しているようです。今回はそのOCaml版という事です。

OCaml Quine解説

上記のOCaml Quineは、文字数を調整するために難読化に近い処理を実施していますので、変数名などを少しずつ解説していきます。

まず、全体の構成は「自分自身をBase64エンコードしたものをデコードして、整形して表示する」ようになっています。 大まかな構成は他言語でも同じように解説していますので、こちらも見ていただくと直感的に分かるかもしれません。

ディレクティブ

Quineの最上部には、利用するライブラリのディレクティブやimportが記述されています。この辺りは横幅の文字数に応じて、実行可能なように順番を入れ替えたり、コードの後半で実行しているものもあります。

#use "topfind";;#require "zstd";;#require "base64";;#require "compiler-libs";;open Printf;;open String;;

元々はOCamlのメジャーなProject管理、ビルドツールであるDuneを利用していたのですが、後述するeval実装の影響でREPLでの実行になり、ディレクティブが必要になってしまっています。 正確には、compiler-libsをduneが呼び出せるようにbyte modeで Toploop.initialize_toplevel_env を呼び出す必要があるのですが、link_flagsと上手く噛み合わずこのような実装に至りました*1discuss.ocaml.org

OCaml eval

OCamlはコード中の改行や空白に強い意味を持つ言語ではないものの、今回のようにアスキーアート状のコードを実行、整形するために「文字列としてソースコードを扱う」実装をしています。 文字列としてアスキーアートとからデコードしたものを、別言語でいうevalで実行する形式です。

「OCaml eval」で検索すると以下のようなStack Overflowとコードが出てきます。 stackoverflow.com

(* evalするやつ *)
let eval code =
    let as_buf = Lexing.from_string code in
    let parsed = !Toploop.parse_toplevel_phrase as_buf in
    ignore (Toploop.execute_phrase true Format.std_formatter parsed);;
eval "let () = print_endline \"hello\";;"

実際にQuineの中でもこのコードを利用しています。

また、文字列置換も沢山使うので、それっぽい関数を前の方で定義しています。

(* 文字列置換するやつ *)
let replace_all_substrings find_str replace_with input_str =
  let pattern = Str.regexp_string find_str in
  Str.global_replace pattern replace_with input_str;;

アスキーアートへの整形

アスキーアートへの整形は、base64エンコード/デコードとzstdによる文字列圧縮を組み合わせて実装しています。

以下がevalされるソースコードの部分を展開したものです。

(* Quineコードのプレースホルダーを置換して実行するする *)
eval {|
    let () =
        (* 元のコードをデコード *)
        (* 最終的には%sありのコード全体をbase64 encodeした結果を、このソースコード内の%sと置換して完成 *)
        let base_text = Base64.decode_exn "%s" in
        let encode_text = Base64.encode_exn base_text in
        let code = replace_all_substrings ("%" ^ "s") encode_text base_text in

        (* AA 2625文字 のコードをデコード *)
        let origin_size = 2625 in
        let base64_input = "KLUv/W..." in
        let compressed_data = Base64.decode_exn base64_input in
        let original_text = Zstd.decompress origin_size compressed_data in

        (* 元のコードをAAに合わせて表示 *)
        let output =
            let buf = Buffer.create origin_size in
            let j = ref 0 in
            String.iter(function
                | '1' -> Buffer.add_char buf ' '
                | '\n' -> Buffer.add_char buf '\n'
                | _ ->
                    Buffer.add_char buf code.[!j];
                    incr j)
                original_text;
            Buffer.contents buf in
        print_endline output;;
|};;

0と1と改行で構成された、ベースとなるアスキーアートはこちらです。

0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000011110000000000000000
0000000000111111111111100000000000000000000001111111111110000000000000000001111111111110000000000000
0000000000001111111111100000000000000000000011111111110000000000000000000111111111111111100000000000
0000000000000001111111110000000000000000000111111111000000000000000000011111000000111111110000000000
0000000000000001111111111100000000000000001111111111000000000000000000011100000000011111110000000000
0000000000000001111111111100000000000000011111111111000000000000000000000000000000011111110000000000
0000000000000001111111111110000000000000111111111111000000000000000000000000000000011111100000000000
0000000000000001111111111111000000000000111111111111000000000000000000000000000000111111000000000000
0000000000000001111011111111100000000001111001111111000000000000000000000000001111111100000000000000
0000000000000001111001111111100000000011110001111111100000000000000000000000111111111111000000000000
0000000000000011110000111111110000000111110001111111100000000000000000000111111111111111110000000000
0000000000000011110000011111111100001111100001111111100000000000000000000000000000111111111000000000
0000000000000011110000001111111110011111000001111111100000000000000000000000000000001111111100000000
0000000000000011110000000111111110111110000001111111100000000000000000000000000000000111111100000000
0000000000000111100000000111111111111100000000111111110000000000000000000000000000000111111100000000
0000000000000111100000000011111111111000000000111111110000000000000000000000000000001111111100000000
0000000000000111100000000001111111110000000000111111110000000000000000111000000000001111111000000000
0000000000000111100000000000111111100000000000111111110000000000000001111000000000011111110000000000
0000000011111111111111000000011111100000001111111111111111100000000001111111111111111111000000000000
0000000011111111111111000000001111000000001111111111111111100000000000011111111111111000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

この事前情報に沿って、ソースコードの文字列を当てはめていく、というロジックになっています。

この手法により、コードを実行した結果同じコードが出力される事が確認できます。

cat quine.ml -> ocaml quine.ml

おわりに

学生時代はLispをよく書いていたものですが、改めてOCamlを学んで見ると新しい学びが多くあり良かったです。 事前に「OCamlはWeb上に情報が少ないよ」と聞いていたのですが、まさにAI時代という事もありClaude CodeやChatGPTに「loopを使うならこう、List.fold_leftを使うならこうだし、Bufferを使うとより短く書けるよ」「Haskellみたいにin使って良い感じに書けるよ」と言われたりしながらスムーズに進める事も出来ました。 すごい時代になったものですね。

Quineも久々に作りましたが、コード自体は小一時間で出来たにもかかわらず、最後の文字数調整でとてつもない時間を費やしてしまいました…。基盤チームでの技術チャレンジも続けていきたい、そんな気持ちになりました。

そんな基盤チームですが、現在採用を加速させておりまして、この記事を1日目としてブログリレーを展開していく予定ですのでお楽しみに。それでは!

We are hiring !!

ギークな学びを渇望している皆さん、エムスリーで一緒に働いてみませんか?

基盤チーム、SREチームでは、医療の横断的な基盤をつくるエンジニアを募集しています! Platform Engineering、Site Reliability Engineeringをやりながら関数型言語もなんでもござれなギークな皆さまをお待ちしています。

エンジニア採用ページはこちら

jobs.m3.com

カジュアル面談もお気軽にどうぞ

jobs.m3.com

インターンも常時募集しています

open.talentio.com

*1:duneの実装もしばらく読んでいたんですがよくわからず上手く動かす方法あれば知りたいです