このための Haskell ライブラリはありませんが、抽象構文ツリーを使用して実装できます。言語に依存しない抽象的な構文ツリーを作成し、そのツリーを同等の Bash スクリプトに変換するバックエンドを適用する、簡単なおもちゃの例を作成します。
Haskell で構文ツリーをモデル化するために、次の 2 つのトリックを使用します。
- GADT を使用して型指定された Bash 式をモデル化する
- free モナドを使って DSL を実装する
GADT のトリックはかなり単純で、いくつかの言語拡張機能を使用して構文を甘くしています。
{-# LANGUAGE GADTs
, FlexibleInstances
, RebindableSyntax
, OverloadedStrings #-}
import Data.String
import Prelude hiding ((++))
type UniqueID = Integer
newtype VStr = VStr UniqueID
newtype VInt = VInt UniqueID
data Expr a where
StrL :: String -> Expr String -- String literal
IntL :: Integer -> Expr Integer -- Integer literal
StrV :: VStr -> Expr String -- String variable
IntV :: VInt -> Expr Integer -- Integer variable
Plus :: Expr Integer -> Expr Integer -> Expr Integer
Concat :: Expr String -> Expr String -> Expr String
Shown :: Expr Integer -> Expr String
instance Num (Expr Integer) where
fromInteger = IntL
(+) = Plus
(*) = undefined
abs = undefined
signum = undefined
instance IsString (Expr String) where
fromString = StrL
(++) :: Expr String -> Expr String -> Expr String
(++) = Concat
これにより、DSL で型指定された Bash 式を構築できます。いくつかの基本的な操作を実装しただけですが、他の操作でどのように拡張できるかは容易に想像できます。
言語拡張を使用しなかった場合、次のような式を記述できます。
Concat (StrL "Test") (Shown (Plus (IntL 4) (IntL 5))) :: Expr String
これは大丈夫ですが、あまりセクシーではありません。上記のコードではRebindableSyntax
、数値リテラルをオーバーライドして、次のように置き換えることができ(IntL n)
ますn
。
Concat (StrL "Test") (Shown (Plus 4 5)) :: Expr String
同様に、を使用して数値リテラルを追加できるように、Expr Integer
実装しました。Num
+
Concat (StrL "Test") (Shown (4 + 5)) :: Expr String
同様に、すべての出現箇所を単にOverloadedStrings
に置き換えることができるように使用します。(StrL str)
str
Concat "Test" (Shown (4 + 5)) :: Expr String
また、Prelude(++)
演算子をオーバーライドして、式を Haskell 文字列であるかのように連結できるようにします。
"Test" ++ Shown (4 + 5) :: Expr String
Shown
整数から文字列へのキャスト以外は、ネイティブの Haskell コードのように見えます。きちんとした!
ここで、できればMonad
シンタックス シュガーを使用して、ユーザー フレンドリーな DSL を作成する方法が必要です。ここでフリーモナドの出番です。
フリー モナドは、構文ツリーの 1 つのステップを表すファンクタを取り、そこから構文ツリーを作成します。do
おまけとして、これは常にどのファンクターのモナドでもあるため、表記法を使用してこれらの構文ツリーを組み立てることができます。
それを示すために、前のコード セグメントにさらにコードを追加します。
-- This is in addition to the previous code
{-# LANGUAGE DeriveFunctor #-}
import Control.Monad.Free
data ScriptF next
= NewInt (Expr Integer) (VInt -> next)
| NewStr (Expr String ) (VStr -> next)
| SetStr VStr (Expr String ) next
| SetInt VInt (Expr Integer) next
| Echo (Expr String) next
| Exit (Expr Integer)
deriving (Functor)
type Script = Free ScriptF
newInt :: Expr Integer -> Script VInt
newInt n = liftF $ NewInt n id
newStr :: Expr String -> Script VStr
newStr str = liftF $ NewStr str id
setStr :: VStr -> Expr String -> Script ()
setStr v expr = liftF $ SetStr v expr ()
setInt :: VInt -> Expr Integer -> Script ()
setInt v expr = liftF $ SetInt v expr ()
echo :: Expr String -> Script ()
echo expr = liftF $ Echo expr ()
exit :: Expr Integer -> Script r
exit expr = liftF $ Exit expr
ScriptF
ファンクターは、DSL の 1 つのステップを表します 。Free
基本的にステップのリストを作成しScriptF
、これらのステップのリストを組み立てることができるモナドを定義します。liftF
この関数は、1 つのステップを実行し、1 つのアクションでリストを作成するものと考えることができます。
次に、表記法を使用do
してこれらのステップを組み立てることができます。ここで、do
表記法はこれらのアクションのリストを連結します。
script :: Script r
script = do
hello <- newStr "Hello, "
world <- newStr "World!"
setStr hello (StrV hello ++ StrV world)
echo ("hello: " ++ StrV hello)
echo ("world: " ++ StrV world)
x <- newInt 4
y <- newInt 5
exit (IntV x + IntV y)
これは、定義したばかりのプリミティブ ステップを組み立てる方法を示しています。これには、次のようなモナドコンビネータのサポートを含む、モナドのすべての優れたプロパティがありますforM_
。
import Control.Monad
script2 :: Script ()
script2 = forM_ [1..5] $ \i -> do
x <- newInt (IntL i)
setInt x (IntV x + 5)
echo (Shown (IntV x))
Script
ターゲット言語が型付けされていない場合でも、モナドがどのように型安全性を強制するかに注目してください。String
が予期される場所で誤ってリテラルを使用しInteger
たり、その逆を行ったりすることはできません。のようなタイプセーフな変換を使用して、それらの間で明示的に変換する必要がありますShown
。
Script
また、モナドは exit ステートメントの後のすべてのコマンドを飲み込むことに注意してください。インタープリターに到達する前に無視されます。もちろん、後続のステップExit
を受け入れるようにコンストラクターを書き直すことで、この動作を変更できます。next
これらの抽象構文木は純粋です。つまり、それらを純粋に調べて解釈することができます。Script
モナドを同等の Bash スクリプトに変換する Bash バックエンドなど、いくつかのバックエンドを定義できます。
bashExpr :: Expr a -> String
bashExpr expr = case expr of
StrL str -> str
IntL int -> show int
StrV (VStr nID) -> "${S" <> show nID <> "}"
IntV (VInt nID) -> "${I" <> show nID <> "}"
Plus expr1 expr2 ->
concat ["$((", bashExpr expr1, "+", bashExpr expr2, "))"]
Concat expr1 expr2 -> bashExpr expr1 <> bashExpr expr2
Shown expr' -> bashExpr expr'
bashBackend :: Script r -> String
bashBackend script = go 0 0 script where
go nStrs nInts script =
case script of
Free f -> case f of
NewInt e k ->
"I" <> show nInts <> "=" <> bashExpr e <> "\n" <>
go nStrs (nInts + 1) (k (VInt nInts))
NewStr e k ->
"S" <> show nStrs <> "=" <> bashExpr e <> "\n" <>
go (nStrs + 1) nInts (k (VStr nStrs))
SetStr (VStr nID) e script' ->
"S" <> show nID <> "=" <> bashExpr e <> "\n" <>
go nStrs nInts script'
SetInt (VInt nID) e script' ->
"I" <> show nID <> "=" <> bashExpr e <> "\n" <>
go nStrs nInts script'
Echo e script' ->
"echo " <> bashExpr e <> "\n" <>
go nStrs nInts script'
Exit e ->
"exit " <> bashExpr e <> "\n"
Pure _ -> ""
2 つのインタープリターを定義しました。1 つは式構文ツリー用で、もう 1 つはモナディック DSL 構文ツリー用です。これら 2 つのインタープリターは、言語に依存しないプログラムを、文字列として表される同等の Bash プログラムにコンパイルします。もちろん、表現の選択は完全にあなた次第です。
Script
このインタープリターは、モナドが新しい変数を要求するたびに、新しい一意の変数を自動的に作成します。
このインタープリターを試して、動作するかどうかを確認しましょう:
>>> putStr $ bashBackend script
S0=Hello,
S1=World!
S0=${S0}${S1}
echo hello: ${S0}
echo world: ${S1}
I0=4
I1=5
exit $((${I0}+${I1}))
同等の言語に依存しないプログラムを実行する bash スクリプトを生成します。同様に、それscript2
もうまく翻訳されます。
>>> putStr $ bashBackend script2
I0=1
I0=$((${I0}+5))
echo ${I0}
I1=2
I1=$((${I1}+5))
echo ${I1}
I2=3
I2=$((${I2}+5))
echo ${I2}
I3=4
I3=$((${I3}+5))
echo ${I3}
I4=5
I4=$((${I4}+5))
echo ${I4}
したがって、これは明らかに包括的ではありませんが、うまくいけば、Haskell でこれを慣用的に実装する方法についてのアイデアが得られることを願っています。free モナドの使い方についてもっと知りたい場合は、以下を読むことをお勧めします:
また、ここに完全なコードを添付しました。
{-# LANGUAGE GADTs
, FlexibleInstances
, RebindableSyntax
, DeriveFunctor
, OverloadedStrings #-}
import Control.Monad.Free
import Control.Monad
import Data.Monoid
import Data.String
import Prelude hiding ((++))
type UniqueID = Integer
newtype VStr = VStr UniqueID
newtype VInt = VInt UniqueID
data Expr a where
StrL :: String -> Expr String -- String literal
IntL :: Integer -> Expr Integer -- Integer literal
StrV :: VStr -> Expr String -- String variable
IntV :: VInt -> Expr Integer -- Integer variable
Plus :: Expr Integer -> Expr Integer -> Expr Integer
Concat :: Expr String -> Expr String -> Expr String
Shown :: Expr Integer -> Expr String
instance Num (Expr Integer) where
fromInteger = IntL
(+) = Plus
(*) = undefined
abs = undefined
signum = undefined
instance IsString (Expr String) where
fromString = StrL
(++) :: Expr String -> Expr String -> Expr String
(++) = Concat
data ScriptF next
= NewInt (Expr Integer) (VInt -> next)
| NewStr (Expr String ) (VStr -> next)
| SetStr VStr (Expr String ) next
| SetInt VInt (Expr Integer) next
| Echo (Expr String) next
| Exit (Expr Integer)
deriving (Functor)
type Script = Free ScriptF
newInt :: Expr Integer -> Script VInt
newInt n = liftF $ NewInt n id
newStr :: Expr String -> Script VStr
newStr str = liftF $ NewStr str id
setStr :: VStr -> Expr String -> Script ()
setStr v expr = liftF $ SetStr v expr ()
setInt :: VInt -> Expr Integer -> Script ()
setInt v expr = liftF $ SetInt v expr ()
echo :: Expr String -> Script ()
echo expr = liftF $ Echo expr ()
exit :: Expr Integer -> Script r
exit expr = liftF $ Exit expr
script :: Script r
script = do
hello <- newStr "Hello, "
world <- newStr "World!"
setStr hello (StrV hello ++ StrV world)
echo ("hello: " ++ StrV hello)
echo ("world: " ++ StrV world)
x <- newInt 4
y <- newInt 5
exit (IntV x + IntV y)
script2 :: Script ()
script2 = forM_ [1..5] $ \i -> do
x <- newInt (IntL i)
setInt x (IntV x + 5)
echo (Shown (IntV x))
bashExpr :: Expr a -> String
bashExpr expr = case expr of
StrL str -> str
IntL int -> show int
StrV (VStr nID) -> "${S" <> show nID <> "}"
IntV (VInt nID) -> "${I" <> show nID <> "}"
Plus expr1 expr2 ->
concat ["$((", bashExpr expr1, "+", bashExpr expr2, "))"]
Concat expr1 expr2 -> bashExpr expr1 <> bashExpr expr2
Shown expr' -> bashExpr expr'
bashBackend :: Script r -> String
bashBackend script = go 0 0 script where
go nStrs nInts script =
case script of
Free f -> case f of
NewInt e k ->
"I" <> show nInts <> "=" <> bashExpr e <> "\n" <>
go nStrs (nInts + 1) (k (VInt nInts))
NewStr e k ->
"S" <> show nStrs <> "=" <> bashExpr e <> "\n" <>
go (nStrs + 1) nInts (k (VStr nStrs))
SetStr (VStr nID) e script' ->
"S" <> show nID <> "=" <> bashExpr e <> "\n" <>
go nStrs nInts script'
SetInt (VInt nID) e script' ->
"I" <> show nID <> "=" <> bashExpr e <> "\n" <>
go nStrs nInts script'
Echo e script' ->
"echo " <> bashExpr e <> "\n" <>
go nStrs nInts script'
Exit e ->
"exit " <> bashExpr e <> "\n"
Pure _ -> ""