使用诸如解析器之类的东西使用 Hasql 语句输出

Consuming Hasql statement outputs using something like a parser

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

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 =  :: 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),我们在其中使用输入流,然后想要编写解析函数,从该流中获取一些“令牌”并将 return 结果包装在解析函子(我认为它真的应该是一个 Monad)。唯一的区别是,由于这些结果是嵌套的,它们还需要使用先前解析器的输出(尽管实际上您也可以使用 Megaparsec 和 Control.Applicative 来执行此操作)。这将允许更小的函数 mkCountrymkTownmkAuthor 等,它们可以由 <*><$>.

组成

所以,我的问题基本上是双重的: (1) 对于此类实际应用程序,这是一种合理的(甚至是常见的)方法吗,或者我是否缺少某种明显的优化,这将使该代码更具可组合性; (2) 如果我要实现这个,是使 Megaparsec 适应工作的好方法(我认为基本上是为查询结果编写一个标记器), 会更简单吗包含查询结果和输出值的数据类型,然后添加 MonadApplicative 实例定义?

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

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

如果您在此类函数中发现了模式,那么这些模式也可能是特定领域的。没有什么比将它们包装在其他纯函数中并通过简单地调用它们来组合更好的建议了。不需要应用程序或 monad。

关于解析库和标记化,我真的不明白它与所讨论的问题有什么关系,但我可能没有理解你的意思。另外我不建议引入镜头来解决这样一个微不足道的问题,你最终可能会得到一个更复杂、更难维护的解决方案。