如何使用 Haskell 中的随机性来生成 JSON "model" 的实例?
How to use randomness in Haskell to produce instances of a JSON "model"?
我有这项工作,我必须从文件中读取 JSON 并根据其模型生成它的实例。我正在使用 aeson 来序列化对象,但我在处理生成新对象的随机性方面遇到了很大的问题。
根据我从文件中得到的内容生成一个新的 JSON 非常简单:
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.ByteString.Lazy.Char8 as ByteString
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Types
import qualified Data.Text
read :: String -> IO ()
read filePath = do
json <- readFile filePath
let Just parsedJSON =
Data.Aeson.decode $ ByteString.pack json :: Maybe Aeson.Object
let newJSON = fmap valueMapper parsedJSON
print $ Aeson.encode newJSON
valueMapper :: Types.Value -> Types.Value
valueMapper value =
case value of
Types.String _ -> Types.String "randomValue"
Types.Number _ -> Types.Number 0
Types.Object object -> Types.Object $ fmap valueMapper object
Types.Array array -> Types.Array $ fmap valueMapper array
我的第一次尝试是在 IO 之外产生随机值。我使用了这个功能:
randomStr :: String
randomStr = take 10 $ randomRs ('a','z') $ unsafePerformIO newStdGen
穿上 valueMapper
:
valueMapper :: Types.Value -> Types.Value
valueMapper value =
case value of
Types.String _ -> Types.String $ Data.Text.pack randomStr
Types.Number _ -> Types.Number 0
Types.Object object -> Types.Object $ fmap valueMapper object
Types.Array array -> Types.Array $ fmap valueMapper array
这个 "works",但是所有生成的字符串对于每个 String
字段都是相同的。
经过一些研究,我发现如果我想为每个 String
事件产生不同的值,我必须使用 IO:
randomStr :: IO String
randomStr = replicateM 10 (randomRIO ('a', 'z'))
现在,我知道 randomStr
的每次调用都有不同的字符串...但我也有类型不匹配的问题。 Value
的 Aeson String
构造函数采用 Data.Text
,但我拥有的是 IO String
。据我所知,我的字符串永远无法从 IO
.
返回
我不知道是否有办法(希望如此)使用最新的 randomStr
来组成我的新 JSON 对象。我也不知道我的方法是否好。我愿意接受有关如何以我的方式或任何其他方式将其付诸实践的建议(关于如何编写更好的代码的一些技巧也很棒)。
在编写 Haskell 代码时发现自己需要将纯代码块转换为 IO 操作(或其他操作中的单子代码)的情况相对常见。它伴随着实践(并且,根据评论,在阅读了很多教程之后),但我可以向您展示我在处理您的代码示例时的思考过程。
如您所见,尝试使用 unsafePerformIO
"hide" IO 是一个糟糕的主意。正确的选择是重写整个事情以在 IO monad 中操作,即使——如您所见——将 randomStr :: String
重写为 randomStr :: IO String
会启动一系列类型不匹配错误,这些错误需要一直解决到顶部。
所以,让我们来解决它们。如果 valueMapper
要使用 randomStr :: IO String
,它也需要在 IO monad 中运行:
valueMapper :: Types.Value -> IO Types.Value
(注意:如果您在使用实时类型检查 IDE 时进行此更改,您会发现 read
中对 valueMapper
的调用现在被标记为类型错误,与 case 语句中的四个分支一样。)
总之,valueMapper
的外部结构没有问题,即参数大小写匹配:
valueMapper value =
case value of
Types.String _ -> ???
Types.Number _ -> ???
Types.Object object -> ???
Types.Array array -> ???
区别在于现在每个 ???
都需要 return 一个 IO Types.Value
而不是 Types.Value
。让我们从一个简单的开始。假设我们还不打算生成随机数,所以我们只想转换分支:
Types.Number _ -> Types.Number 0 -- pure version
到 IO。在这里,我们有一个纯值 Types.Number 0 :: Types.Value
,我们想要一个 monadic 版本。这就是 return
的用途:
Types.Number _ -> return (Types.Number 0) -- IO version
下一个最简单的是字符串分支。现在,它看起来像:
Types.String _ -> Types.String $ Data.Text.pack randomStr
其中 randomStr
是一个 IO String
。不过,它仍然是一堆类型错误。那是因为 randomStr
是一个 IO String
,我们想把它转换成一个 IO Types.Value
,但是构造:
Types.String $ Data.Text.pack _
正在尝试将 String
直接转换为 Types.Value
。这是使用 monadic 值时的常见问题。我们有一个要转换为 IO b
的 IO a
,但我们只有一个函数(此处为 Types.String . Data.Text.pack
)来执行直接转换 a -> b
。如果我们有一些带有签名的适配器函数会很有帮助:
foo :: (a -> b) -> IO a -> IO b
幸运的是,因为 IO 和所有 monad 一样,也是一个仿函数,我们确实有这样一个适配器:
fmap :: (a -> b) -> IO a -> IO b
所以,我们可以把分支写成:
Types.String _ -> fmap (Types.String . Data.Text.pack) randomStr
最后两个,对于对象和数组,更难,尽管它们的解决方案是相同的。采取对象分支,它的纯版本看起来像:
Types.Object object -> Types.Object $ fmap valueMapper object
在纯版本中,fmap
用于将valueMapper :: Types.Value -> Types.Value
应用于列表object :: [Types.Value]
的每个元素,以得到一个新的[Types.Value]
类型的列表,然后使用 Types.Object
构造函数转换为 Types.Value
。
让我们先解决 fmap
。我们仍然会从我们的纯参数 value
的大小写匹配中得到 object :: [Types.Value]
,所以这没有改变。但是我们想将函数 valueMapper :: Types.Value -> IO Types.Value
应用于 object
的每个元素。结果将是 Types.Value
的列表,但在 IO monad 上下文中,因此完整的结果类型将为 IO [Types.Value]
。即我们要一个适配器函数:
bar :: (a -> IO a) -> [a] -> IO [a]
^^^^^^^^^^^ ^^^ ^^^^^^- output list in IO context
| `- input list
`- element-by-element conversion
这样的函数已经以更一般的形式存在,如 traverse
。完整的签名是:
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
但专用于可遍历列表和 IO 应用程序,它是:
traverse :: (a -> IO b) -> [a] -> IO [b]
在我们的对象分支中使用它,结果将如下所示:
Types.Object object -> Types.Object $ traverse valueMapper object
这仍然会出现类型错误,因为 traverse valueMapper object
return 是一个 IO [Types.Value]
,而我们正尝试在实际需要转换的地方使用直接转换 Types.Object :: [Types.Value] -> Types.Value
IO [Types.Value] -> IO Types.Value
。这与我们在上面的字符串分支中遇到的问题相同,解决方案是使用 fmap
,因此进行以下类型检查:
Types.Object object -> fmap Types.Object (traverse valueMapper object)
您可能想在这里花点时间对数组分支执行等效转换。
通过这些更改,valueMapper
将进行类型检查。现在唯一的问题是它在 read
中的用法不会进行类型检查。问题是行:
let newJSON = fmap valueMapper parsedJSON
这里 parsedJSON
是 Aeson.Object
又名 Types.Object
类型,实际上是 HashMap Text Types.Value
的别名。这里的 fmap
用于依次将纯 valueMapper :: Types.Value -> Types.Value
应用于每个 hashmap 元素。
现在,我们要依次将 valueMapper :: Types.Value -> IO Types.Value
应用于每个元素,并在 IO 上下文中得到整个结果作为 IO Aeson.Object
。幸运的是,Aeson.Object
AKA HashMap
是 Traversable
,所以这里的解决方案与之前的对象和数组分支相同——将 fmap
替换为 traverse
:
let newJSON = traverse valueMapper parsedJSON
这仍然无法正常工作,因为下一行:
print $ Aeson.encode newJSON
期望 newJSON
是一个纯 Aeson.Object
,但是 traverse
调用的 return 值在 IO 上下文中,所以它是 IO Aeson.Object
.我们可以尝试重写此 print
行以期望 newJSON :: IO Aeson.Object
。例如,以下将起作用:
print =<< fmap Aeson.encode newJSON
不过,其实还有更简单的方法。在 do 块中,左箭头 <-
符号可用于此目的。其中:
let newJSON = traverse valueMapper parsedJSON
分配 newJSON
类型为 IO Aeson.Object
的 IO 操作,备选方案:
newJSON <- traverse valueMapper parsedJSON
"unwraps" 分配 newJSON
底层 Aeson.Object
的 IO 操作,以便在后续语句中使用。所以:
newJSON <- traverse valueMapper parsedJSON
print $ Aeson.encode newJSON
将进行类型检查。
还有一个文体注释。通常使用中缀同义词 <$>
代替 fmap
将纯函数应用于 IO 操作。因此,最终程序将如下所示:
import qualified Data.ByteString.Lazy.Char8 as ByteString
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Types
import qualified Data.Text
import Control.Monad
import System.Random
read :: String -> IO ()
read filePath = do
json <- readFile filePath
let Just parsedJSON =
Aeson.decode $ ByteString.pack json :: Maybe Aeson.Object
newJSON <- traverse valueMapper parsedJSON
print $ Aeson.encode newJSON
valueMapper :: Types.Value -> IO Types.Value
valueMapper value =
case value of
Types.String _ -> Types.String . Data.Text.pack <$> randomStr
Types.Number _ -> return $ Types.Number 0
Types.Object object -> Types.Object <$> traverse valueMapper object
Types.Array array -> Types.Array <$> traverse valueMapper array
randomStr :: IO String
randomStr = replicateM 10 (randomRIO ('a', 'z'))
我有这项工作,我必须从文件中读取 JSON 并根据其模型生成它的实例。我正在使用 aeson 来序列化对象,但我在处理生成新对象的随机性方面遇到了很大的问题。
根据我从文件中得到的内容生成一个新的 JSON 非常简单:
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.ByteString.Lazy.Char8 as ByteString
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Types
import qualified Data.Text
read :: String -> IO ()
read filePath = do
json <- readFile filePath
let Just parsedJSON =
Data.Aeson.decode $ ByteString.pack json :: Maybe Aeson.Object
let newJSON = fmap valueMapper parsedJSON
print $ Aeson.encode newJSON
valueMapper :: Types.Value -> Types.Value
valueMapper value =
case value of
Types.String _ -> Types.String "randomValue"
Types.Number _ -> Types.Number 0
Types.Object object -> Types.Object $ fmap valueMapper object
Types.Array array -> Types.Array $ fmap valueMapper array
我的第一次尝试是在 IO 之外产生随机值。我使用了这个功能:
randomStr :: String
randomStr = take 10 $ randomRs ('a','z') $ unsafePerformIO newStdGen
穿上 valueMapper
:
valueMapper :: Types.Value -> Types.Value
valueMapper value =
case value of
Types.String _ -> Types.String $ Data.Text.pack randomStr
Types.Number _ -> Types.Number 0
Types.Object object -> Types.Object $ fmap valueMapper object
Types.Array array -> Types.Array $ fmap valueMapper array
这个 "works",但是所有生成的字符串对于每个 String
字段都是相同的。
经过一些研究,我发现如果我想为每个 String
事件产生不同的值,我必须使用 IO:
randomStr :: IO String
randomStr = replicateM 10 (randomRIO ('a', 'z'))
现在,我知道 randomStr
的每次调用都有不同的字符串...但我也有类型不匹配的问题。 Value
的 Aeson String
构造函数采用 Data.Text
,但我拥有的是 IO String
。据我所知,我的字符串永远无法从 IO
.
我不知道是否有办法(希望如此)使用最新的 randomStr
来组成我的新 JSON 对象。我也不知道我的方法是否好。我愿意接受有关如何以我的方式或任何其他方式将其付诸实践的建议(关于如何编写更好的代码的一些技巧也很棒)。
在编写 Haskell 代码时发现自己需要将纯代码块转换为 IO 操作(或其他操作中的单子代码)的情况相对常见。它伴随着实践(并且,根据评论,在阅读了很多教程之后),但我可以向您展示我在处理您的代码示例时的思考过程。
如您所见,尝试使用 unsafePerformIO
"hide" IO 是一个糟糕的主意。正确的选择是重写整个事情以在 IO monad 中操作,即使——如您所见——将 randomStr :: String
重写为 randomStr :: IO String
会启动一系列类型不匹配错误,这些错误需要一直解决到顶部。
所以,让我们来解决它们。如果 valueMapper
要使用 randomStr :: IO String
,它也需要在 IO monad 中运行:
valueMapper :: Types.Value -> IO Types.Value
(注意:如果您在使用实时类型检查 IDE 时进行此更改,您会发现 read
中对 valueMapper
的调用现在被标记为类型错误,与 case 语句中的四个分支一样。)
总之,valueMapper
的外部结构没有问题,即参数大小写匹配:
valueMapper value =
case value of
Types.String _ -> ???
Types.Number _ -> ???
Types.Object object -> ???
Types.Array array -> ???
区别在于现在每个 ???
都需要 return 一个 IO Types.Value
而不是 Types.Value
。让我们从一个简单的开始。假设我们还不打算生成随机数,所以我们只想转换分支:
Types.Number _ -> Types.Number 0 -- pure version
到 IO。在这里,我们有一个纯值 Types.Number 0 :: Types.Value
,我们想要一个 monadic 版本。这就是 return
的用途:
Types.Number _ -> return (Types.Number 0) -- IO version
下一个最简单的是字符串分支。现在,它看起来像:
Types.String _ -> Types.String $ Data.Text.pack randomStr
其中 randomStr
是一个 IO String
。不过,它仍然是一堆类型错误。那是因为 randomStr
是一个 IO String
,我们想把它转换成一个 IO Types.Value
,但是构造:
Types.String $ Data.Text.pack _
正在尝试将 String
直接转换为 Types.Value
。这是使用 monadic 值时的常见问题。我们有一个要转换为 IO b
的 IO a
,但我们只有一个函数(此处为 Types.String . Data.Text.pack
)来执行直接转换 a -> b
。如果我们有一些带有签名的适配器函数会很有帮助:
foo :: (a -> b) -> IO a -> IO b
幸运的是,因为 IO 和所有 monad 一样,也是一个仿函数,我们确实有这样一个适配器:
fmap :: (a -> b) -> IO a -> IO b
所以,我们可以把分支写成:
Types.String _ -> fmap (Types.String . Data.Text.pack) randomStr
最后两个,对于对象和数组,更难,尽管它们的解决方案是相同的。采取对象分支,它的纯版本看起来像:
Types.Object object -> Types.Object $ fmap valueMapper object
在纯版本中,fmap
用于将valueMapper :: Types.Value -> Types.Value
应用于列表object :: [Types.Value]
的每个元素,以得到一个新的[Types.Value]
类型的列表,然后使用 Types.Object
构造函数转换为 Types.Value
。
让我们先解决 fmap
。我们仍然会从我们的纯参数 value
的大小写匹配中得到 object :: [Types.Value]
,所以这没有改变。但是我们想将函数 valueMapper :: Types.Value -> IO Types.Value
应用于 object
的每个元素。结果将是 Types.Value
的列表,但在 IO monad 上下文中,因此完整的结果类型将为 IO [Types.Value]
。即我们要一个适配器函数:
bar :: (a -> IO a) -> [a] -> IO [a]
^^^^^^^^^^^ ^^^ ^^^^^^- output list in IO context
| `- input list
`- element-by-element conversion
这样的函数已经以更一般的形式存在,如 traverse
。完整的签名是:
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
但专用于可遍历列表和 IO 应用程序,它是:
traverse :: (a -> IO b) -> [a] -> IO [b]
在我们的对象分支中使用它,结果将如下所示:
Types.Object object -> Types.Object $ traverse valueMapper object
这仍然会出现类型错误,因为 traverse valueMapper object
return 是一个 IO [Types.Value]
,而我们正尝试在实际需要转换的地方使用直接转换 Types.Object :: [Types.Value] -> Types.Value
IO [Types.Value] -> IO Types.Value
。这与我们在上面的字符串分支中遇到的问题相同,解决方案是使用 fmap
,因此进行以下类型检查:
Types.Object object -> fmap Types.Object (traverse valueMapper object)
您可能想在这里花点时间对数组分支执行等效转换。
通过这些更改,valueMapper
将进行类型检查。现在唯一的问题是它在 read
中的用法不会进行类型检查。问题是行:
let newJSON = fmap valueMapper parsedJSON
这里 parsedJSON
是 Aeson.Object
又名 Types.Object
类型,实际上是 HashMap Text Types.Value
的别名。这里的 fmap
用于依次将纯 valueMapper :: Types.Value -> Types.Value
应用于每个 hashmap 元素。
现在,我们要依次将 valueMapper :: Types.Value -> IO Types.Value
应用于每个元素,并在 IO 上下文中得到整个结果作为 IO Aeson.Object
。幸运的是,Aeson.Object
AKA HashMap
是 Traversable
,所以这里的解决方案与之前的对象和数组分支相同——将 fmap
替换为 traverse
:
let newJSON = traverse valueMapper parsedJSON
这仍然无法正常工作,因为下一行:
print $ Aeson.encode newJSON
期望 newJSON
是一个纯 Aeson.Object
,但是 traverse
调用的 return 值在 IO 上下文中,所以它是 IO Aeson.Object
.我们可以尝试重写此 print
行以期望 newJSON :: IO Aeson.Object
。例如,以下将起作用:
print =<< fmap Aeson.encode newJSON
不过,其实还有更简单的方法。在 do 块中,左箭头 <-
符号可用于此目的。其中:
let newJSON = traverse valueMapper parsedJSON
分配 newJSON
类型为 IO Aeson.Object
的 IO 操作,备选方案:
newJSON <- traverse valueMapper parsedJSON
"unwraps" 分配 newJSON
底层 Aeson.Object
的 IO 操作,以便在后续语句中使用。所以:
newJSON <- traverse valueMapper parsedJSON
print $ Aeson.encode newJSON
将进行类型检查。
还有一个文体注释。通常使用中缀同义词 <$>
代替 fmap
将纯函数应用于 IO 操作。因此,最终程序将如下所示:
import qualified Data.ByteString.Lazy.Char8 as ByteString
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Types
import qualified Data.Text
import Control.Monad
import System.Random
read :: String -> IO ()
read filePath = do
json <- readFile filePath
let Just parsedJSON =
Aeson.decode $ ByteString.pack json :: Maybe Aeson.Object
newJSON <- traverse valueMapper parsedJSON
print $ Aeson.encode newJSON
valueMapper :: Types.Value -> IO Types.Value
valueMapper value =
case value of
Types.String _ -> Types.String . Data.Text.pack <$> randomStr
Types.Number _ -> return $ Types.Number 0
Types.Object object -> Types.Object <$> traverse valueMapper object
Types.Array array -> Types.Array <$> traverse valueMapper array
randomStr :: IO String
randomStr = replicateM 10 (randomRIO ('a', 'z'))