没有为此的 Haskell 库,但您可以使用抽象语法树来实现它。我将构建一个简单的玩具示例,它构建一个与语言无关的抽象语法树,然后应用一个将树转换为等效 Bash 脚本的后端。
我将使用两个技巧在 Haskell 中对语法树进行建模:
- 使用 GADT 对类型化的 Bash 表达式进行建模
- 使用免费的 monad 实现 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)
用 just替换所有出现的str
:
Concat "Test" (Shown (4 + 5)) :: Expr String
我还覆盖了 Prelude(++)
运算符,以便我们可以连接表达式,就好像它们是 Haskell 字符串一样:
"Test" ++ Shown (4 + 5) :: Expr String
除了Shown
从整数到字符串的转换之外,它看起来就像原生的 Haskell 代码。整洁的!
现在我们需要一种方法来创建用户友好的 DSL,最好使用Monad
语法糖。这就是免费单子的用武之地。
一个自由的 monad 采用一个代表语法树中单个步骤的函子,并从中创建一个语法树。作为奖励,它始终是任何函子的 monad,因此您可以使用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
函子代表了我们 DSL 中的ScriptF
一个步骤。 Free
本质上创建了一个ScriptF
步骤列表并定义了一个 monad,我们可以在其中组装这些步骤的列表。您可以将该liftF
功能视为采取一个步骤并通过一个操作创建一个列表。
然后我们可以使用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)
这显示了我们如何组装我们刚刚定义的原始步骤。这具有 monad 的所有优点,包括对 monadic 组合子的支持,例如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
monad 是如何强制类型安全的,即使我们的目标语言可能是无类型的。您不能String
在期望的地方意外使用文字,Integer
反之亦然。您必须使用类型安全的转换(如Shown
.
另请注意,Script
monad 会在 exit 语句之后吞下任何命令。他们甚至在到达口译员之前就被忽略了。当然,您可以通过重写Exit
构造函数以接受后续next
步骤来更改此行为。
这些抽象语法树是纯粹的,这意味着我们可以纯粹地检查和解释它们。我们可以定义多个后端,例如将我们的Script
monad 转换为等效 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 _ -> ""
我定义了两个解释器:一个用于表达式语法树,一个用于 monadic DSL 语法树。这两个解释器将任何与语言无关的程序编译成等效的 Bash 程序,以字符串表示。当然,代表的选择完全取决于您。
每次我们的Script
monad 请求一个新变量时,这个解释器都会自动创建新的唯一变量。
让我们试试这个解释器,看看它是否有效:
>>> 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 中惯用地实现它。如果您想了解更多关于使用免费 monad 的信息,我建议您阅读:
我还在这里附上了完整的代码:
{-# 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 _ -> ""