使用类似解析器的东西消费Hasql语句输出



我有一个应用程序,它使用一些深度嵌套的记录结构来建模数据域。一个做作但类似的例子是这样的:

Book
- Genre
- Author
- Hometown
- Country

我发现,当使用Hasql(或Hasql- th更精确)编写查询时,我结束了这个巨大的函数,它需要一个巨大的元组,并通过有效地消费这个元组尾部优先构造这些嵌套的记录类型来构造我的记录,最后把它放在一起在一个大的类型(包括转换一些原始值等)。它最终看起来像这样:

bookDetailStatement :: Statement BookID (Maybe Book)
bookDetailStatement = dimap
( (BookID a) -> a)    -- extract the actual ID from the container
(fmap mkBook)          -- process the record if it exists
[maybeStatement|
select
(some stuff)
from books
join genres on (...)
join authors on (...)
join towns on (...)
join countries on (...)
where books.id = $1 :: int4
limit 1
|]
mkBook (
-- Book
book_id, book_title, ...
-- Genre
genre_name, ...
-- Author
author_id, author_name, ...
-- Town
town_name, town_coords, ...
-- Country
country_name, ...
) = let {- some data processing -} in Book {..}

这对于编写和维护/重构来说有点烦人,我正在考虑尝试使用Control.Applicative来改造它。这让我想到,这本质上是一种解析器(有点像Megaparsec),我们正在消费输入流,然后想要组成解析函数,这些解析函数需要一些"记号"。从该流中取出并返回包装在解析函函数中的结果(我认为它实际上应该是Monad)。唯一的区别是,由于这些结果是嵌套的,它们还需要消耗先前解析器的输出(尽管实际上您也可以使用Megaparsec和Control.Applicative来实现这一点)。这将允许更小的函数mkCountry,mkTown,mkAuthor等,它们可以由<*><$>组成。

所以,我的问题基本上是双重的:(1)这是一种合理的(甚至是常见的)方法,以这种现实世界的应用程序,或者我错过了某种明显的优化,将允许这段代码更可组合;(2)如果我要实现这一点,是一个很好的路线,以适应Megaparsec的工作(基本上写一个标记器的查询结果,我认为),写一个数据类型包含查询结果和输出值,然后添加MonadApplicative实例定义会更简单吗?

如果我理解正确的话,你的问题是关于通过由较小的块组成来构建mkBook映射函数。

这个函数是做什么的?它将数据从非规范化形式(所有生成字段的元组)映射到由其他结构组成的特定于领域的结构。它是一个非常基本的纯函数,您只需根据域逻辑移动数据即可。这个问题听起来像是一个域问题。因此,它不是通用的,而是特定于您的应用程序领域的,因此尝试抽象它可能既不会导致可重用的抽象,也不会导致更简单的代码库。

如果您在这样的函数中发现模式,那么这些模式也可能是特定于领域的。我的建议是,最好将它们包装在其他纯函数中,并通过简单地调用它们来进行组合。不需要应用程序或单子。

关于解析库和标记化,我真的不知道它与讨论的问题有什么关系,但我可能错过了你的观点。我也不建议用镜头来解决这样一个微不足道的问题,你可能会得到一个更复杂、更难以维护的解决方案。

最新更新