🦇

UNI Langでゆにゆにしよう / Ook!派生言語のインタプリタをHaskellで実装してみた

2022/12/23に公開約14,400字

はじめに

「赤月ゆに[1]」というYouTuberをご存じでしょうか。私は存じ上げています。
赤月ゆにさんへ想いを馳せる際には「ゆにゆに」することが慣例となっています(要出典)
そこで、赤月ゆにさんへ想いを馳せながらチューリング完全なプログラムを実装できるプログラミング言語「UNI Lang」を開発しました。
みなさんも使用して赤月ゆにさんへ想いを馳せてみませんか?

概要

source.uni
ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに! ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに! ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに! ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに. 

上記のソースコードを実行すると下記の出力が得られます。

uni...BIG LOVE...

言語仕様

結論から言うと、UNI LangはOok![2]派生言語です。
Ook!は難解プログラミング言語[3]として有名なBrainf**k[4]から派生した言語で、命令セットについてこれら3つの言語は等価です。

UNI Langの命令セットは下記の通りです。
前述の通りUNI Langの命令セットはBrainf**kおよびOok!と等価のため、それらとの対応も記述します。

UNI Lang Brainf**k Ook! C風
ゆにゆに. ゆにゆに? > Ook. Ook? ptr++;
ゆにゆに? ゆにゆに. < Ook? Ook. ptr--;
ゆにゆに. ゆにゆに. + Ook. Ook. (*ptr)++;
ゆにゆに! ゆにゆに! - Ook! Ook! (*ptr)--;
ゆにゆに. ゆにゆに! , Ook. Ook! *ptr = getChar();
ゆにゆに! ゆにゆに. . Ook! Ook. putChar(*ptr);
ゆにゆに! ゆにゆに? [ Ook! Ook? while(*ptr) {
ゆにゆに? ゆにゆに! ] Ook? Ook! }

今回実装したインタプリタは、ソースコードを先頭から順番に読み進め、これらの命令を実行します。
なお、命令にマッチしない個所はスキップします。
これを利用し、スペースや改行等を挿入し、可読性を向上させることが可能です。

実装

開発言語

本件の開発言語には、全人類のパソコンに実行環境があることで有名なHaskell[5]を採用しました。
本ソースコードはGHC 8.8.3で実行を確認しています。
GHC標準ライブラリのみ使用しているため、Cabalによる追加ライブラリのインストールは不要です。
万が一GHCがインストールされていない場合には、各自でインストール方法を確認してください。
参考までに、筆者はGHCup[6]を使用し環境構築をしています。

前段階(BrainF**k)の実装

まずはインタプリタの実行エンジンを開発するためにBrainf**kのインタプリタを実装します。
Brainf**kとOok!、そしてUNI Langは命令セットが等価なため、最もソースコードのパースが単純なBrainf**kの実装を通じて実行エンジンを完成させることを目的としています。
インタプリタ実装コードは下記の通りです。

bf_base.hs
module Main where

import System.Environment
import Data.Word
import Data.List
import Control.Monad

-----------------
-- entry point
-----------------

main :: IO ()
main = do
  args <- getArgs
  program <- readFile (head args)
  process program

-----------------
-- command
-----------------

pinc :: String
pinc = ">"

pdec :: String
pdec = "<"

vinc :: String
vinc = "+"

vdec :: String
vdec = "-"

cput :: String
cput = "."

cget :: String
cget = ","

lbegin :: String
lbegin = "["

lend :: String
lend = "]"

-----------------
-- enum
-----------------

data Command = PINC | PDEC | VINC | VDEC | CPUT | CGET deriving Show
data Syntax = Step Command Syntax | Loop Syntax Syntax | End deriving Show
data Memory = Memory [Word8] Word8 [Word8] deriving Show

-----------------
-- main
-----------------

process :: String -> IO ()
process program = void $ run initMem (snd $ parse program)

-----------------
-- parser
-----------------

parse :: String -> (String, Syntax)
parse "" = ("", End)
parse program
  | isPrefixOf pinc program = let (program', syntax) = parse $ drop (length pinc) program in (program', Step PINC syntax)
  | isPrefixOf pdec program = let (program', syntax) = parse $ drop (length pdec) program in (program', Step PDEC syntax)
  | isPrefixOf vinc program = let (program', syntax) = parse $ drop (length vinc) program in (program', Step VINC syntax)
  | isPrefixOf vdec program = let (program', syntax) = parse $ drop (length vdec) program in (program', Step VDEC syntax)
  | isPrefixOf cput program = let (program', syntax) = parse $ drop (length cput) program in (program', Step CPUT syntax)
  | isPrefixOf cget program = let (program', syntax) = parse $ drop (length cget) program in (program', Step CGET syntax)
  | isPrefixOf lbegin program = let (program', syntax) = parse $ drop (length lbegin) program; (program'', syntax') = parse program' in (program'', Loop syntax syntax')
  | isPrefixOf lend program = (drop (length lend) program, End)
  | otherwise = parse $ tail program

-----------------
-- engine
-----------------

run :: Memory -> Syntax -> IO Memory
run memory (Step command next) = do
  memory' <- step memory command
  run memory' next
run memory s@(Loop syntax next) = do
  if getMem memory == minBound then do
    run memory next
  else do
    memory' <- run memory syntax
    run memory' s
run memory End = return memory

step :: Memory -> Command -> IO Memory
step memory PINC = return $ incMem memory
step memory PDEC = return $ decMem memory
step memory VINC = let v = getMem memory in return $ if v == maxBound then putMem minBound memory else putMem (succ v) memory
step memory VDEC = let v = getMem memory in return $ if v == minBound then putMem maxBound memory else putMem (pred v) memory
step memory CPUT = putChar (i2c $ getMem memory) >> return memory
step memory CGET = getChar >>= \v -> return $ putMem (c2i v) memory

-----------------
-- memory
-----------------

incMem :: Memory -> Memory
incMem (Memory a b []) = Memory (b:a) minBound []
incMem (Memory a b (c:cs)) = Memory (b:a) c cs

decMem :: Memory -> Memory
decMem (Memory [] b c) = Memory [] minBound (b:c)
decMem (Memory (a:as) b c) = Memory as a (b:c)

getMem :: Memory -> Word8
getMem (Memory _ b _) = b

putMem :: Word8 -> Memory -> Memory
putMem v (Memory a _ c) = Memory a v c

initMem :: Memory
initMem = Memory [] minBound []

-----------------
-- utility
-----------------

i2c :: Word8 -> Char
i2c = toEnum . fromEnum

c2i :: Char -> Word8
c2i = toEnum . fromEnum

実行方法は下記を想定しています。

runghc bf_base.hs source.bf

本実装の特筆すべき個所は、言語仕様による命令の定義の変更を予め想定した実装としている点です。
前述の通り、本実装は実行エンジンの実装が目的であり、ソースコード内のcommandセクションに命令セットをまとめているため、そのセクション内の記述を任意の文字列に変更することで任意の命令セットの実装が容易に実現できます。

UNI Langインタプリタの実装

UNI Langインタプリタの実装は下記の通りです。

bf_uni.hs
module Main where

import System.Environment
import Data.Word
import Data.List
import Control.Monad

-----------------
-- entry point
-----------------

main :: IO ()
main = do
  args <- getArgs
  program <- readFile (head args)
  process program

-----------------
-- command
-----------------

pinc :: String
pinc = "ゆにゆに. ゆにゆに?"

pdec :: String
pdec = "ゆにゆに? ゆにゆに."

vinc :: String
vinc = "ゆにゆに. ゆにゆに."

vdec :: String
vdec = "ゆにゆに! ゆにゆに!"

cput :: String
cput = "ゆにゆに! ゆにゆに."

cget :: String
cget = "ゆにゆに. ゆにゆに!"

lbegin :: String
lbegin = "ゆにゆに! ゆにゆに?"

lend :: String
lend = "ゆにゆに? ゆにゆに!"

-----------------
-- enum
-----------------

data Command = PINC | PDEC | VINC | VDEC | CPUT | CGET deriving Show
data Syntax = Step Command Syntax | Loop Syntax Syntax | End deriving Show
data Memory = Memory [Word8] Word8 [Word8] deriving Show

-----------------
-- main
-----------------

process :: String -> IO ()
process program = void $ run initMem (snd $ parse program)

-----------------
-- parser
-----------------

parse :: String -> (String, Syntax)
parse "" = ("", End)
parse program
  | isPrefixOf pinc program = let (program', syntax) = parse $ drop (length pinc) program in (program', Step PINC syntax)
  | isPrefixOf pdec program = let (program', syntax) = parse $ drop (length pdec) program in (program', Step PDEC syntax)
  | isPrefixOf vinc program = let (program', syntax) = parse $ drop (length vinc) program in (program', Step VINC syntax)
  | isPrefixOf vdec program = let (program', syntax) = parse $ drop (length vdec) program in (program', Step VDEC syntax)
  | isPrefixOf cput program = let (program', syntax) = parse $ drop (length cput) program in (program', Step CPUT syntax)
  | isPrefixOf cget program = let (program', syntax) = parse $ drop (length cget) program in (program', Step CGET syntax)
  | isPrefixOf lbegin program = let (program', syntax) = parse $ drop (length lbegin) program; (program'', syntax') = parse program' in (program'', Loop syntax syntax')
  | isPrefixOf lend program = (drop (length lend) program, End)
  | otherwise = parse $ tail program

-----------------
-- engine
-----------------

run :: Memory -> Syntax -> IO Memory
run memory (Step command next) = do
  memory' <- step memory command
  run memory' next
run memory s@(Loop syntax next) = do
  if getMem memory == minBound then do
    run memory next
  else do
    memory' <- run memory syntax
    run memory' s
run memory End = return memory

step :: Memory -> Command -> IO Memory
step memory PINC = return $ incMem memory
step memory PDEC = return $ decMem memory
step memory VINC = let v = getMem memory in return $ if v == maxBound then putMem minBound memory else putMem (succ v) memory
step memory VDEC = let v = getMem memory in return $ if v == minBound then putMem maxBound memory else putMem (pred v) memory
step memory CPUT = putChar (i2c $ getMem memory) >> return memory
step memory CGET = getChar >>= \v -> return $ putMem (c2i v) memory

-----------------
-- memory
-----------------

incMem :: Memory -> Memory
incMem (Memory a b []) = Memory (b:a) minBound []
incMem (Memory a b (c:cs)) = Memory (b:a) c cs

decMem :: Memory -> Memory
decMem (Memory [] b c) = Memory [] minBound (b:c)
decMem (Memory (a:as) b c) = Memory as a (b:c)

getMem :: Memory -> Word8
getMem (Memory _ b _) = b

putMem :: Word8 -> Memory -> Memory
putMem v (Memory a _ c) = Memory a v c

initMem :: Memory
initMem = Memory [] minBound []

-----------------
-- utility
-----------------

i2c :: Word8 -> Char
i2c = toEnum . fromEnum

c2i :: Char -> Word8
c2i = toEnum . fromEnum

実行方法は下記を想定しています。

runghc bf_uni.hs source.uni

前述のBrainf**kインタプリタとの違いはcommandセクションの命令セットの文字列のみです。
本実装ではHaskellのChar型の範囲の文字群であれば自由な組み合わせで命令を定義することができるため、UNI Langの実装を元としたさらなる派生言語を実装することも可能です。
ただし、命令文字列の重複は許可されませんので注意してください。

UNI Langのソースコードの作成方法

UNI LangはOok!やBrainf**kと命令が等価なため、これらと同じ難解プログラミング言語に分類されます。
そのため、人力でのコーディングは非常に困難です。
一方、Brainf**kはジョークしてもよく用いられるためか一定の人気があり、ソースコードのジェネレータが多数開発されています。
そこで、本件では実装サポートツールとして、Brainf**kのソースコードをUNI Langのソースコードに変換するコンバータを実装しました。
任意のBrainf**kのソースコードジェネレータの出力結果に対し、このコンバータを用いてトランスパイルする運用を想定しています。
コンバータの実装は下記の通りです。

converter.hs
module Main where

import Data.List

main :: IO ()
main = interact solve

solve :: String -> String
solve = concat . intersperse " " . map tr
  where
    tr '>' = "ゆにゆに. ゆにゆに?"
    tr '<' = "ゆにゆに? ゆにゆに."
    tr '+' = "ゆにゆに. ゆにゆに."
    tr '-' = "ゆにゆに! ゆにゆに!"
    tr ',' = "ゆにゆに. ゆにゆに!"
    tr '.' = "ゆにゆに! ゆにゆに."
    tr '[' = "ゆにゆに! ゆにゆに?"
    tr ']' = "ゆにゆに? ゆにゆに!"
    tr c = [c]

実行方法は下記を想定しています。

runghc converter.hs < source.bf > source.uni

このコンバータに対し、下記のBrainf**kのソースコードを入力すると、

source.bf
----[-->+++++<]>-.-------.-----.----[->+++<]>-...--[-->+++<]>.+++++++.--.+[->++++<]>.++++++[->++<]>.+++.+++++++.--[----->+<]>+.[--->++<]>...

下記のUNI Langのソースコードが出力されます(記事冒頭に記述したソースコードと同一です)

source.uni
ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに! ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに! ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに! ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに? ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに. ゆにゆに? ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに? ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに. ゆにゆに! ゆにゆに. 

改めて、上記のソースコードの実行結果は下記のとおりです。

uni...BIG LOVE...

結論

本件では赤月ゆにさんへ想いを馳せつつ、副作用としてプログラムとしても動作する言語UNI Langを設計し、Haskellを用いてそれを実行するためのインタプリタを実装しました。
UNI Langの派生元であるOok!やBrainf**kは、文字列のパースとそれに基づく命令構文木を作成するトレーニングの題材としても非常によく用いられています。
プログラミングのトレーニングの一環として、ぜひUNI Langの実行環境を実装し赤月ゆにさんへ想いを馳せてみてはいかがでしょうか。

謝辞

本記事を作成するに当たり、下記の方々の名前を使用いたしました(敬称略)
謹んで御礼を申し上げます。

  • 赤月ゆに, ライヴラリ所属, 株式会社ゆにクリエイト

本記事を作成するに当たり、下記の言語について取り扱いました。
謹んで御礼を申し上げます。

  • Haskell, Haskell.org
脚注
  1. 赤月ゆにさんのTwitterアカウント YouTubeチャンネル ↩︎

  2. Ook! ↩︎

  3. 難解プログラミング言語 ↩︎

  4. Brainf**k ↩︎

  5. Haskell ↩︎

  6. GHCup ↩︎

Discussion

ログインするとコメントできます