0

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

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),我们正在使用输入流,然后想要组合解析函数,这些函数从该流中获取一些“令牌”并返回包装在 Parsing Functor 中的结果(我认为这真的应该是一个 Monad)。唯一的区别是,由于这些结果是嵌套的,它们还需要使用以前解析器的输出(尽管实际上您也可以使用 Megaparsec 和使用 来做到这一点Control.Applicative)。这将允许更小的函数mkCountry, mkTown,mkAuthor等可以与<*>和组成<$>

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

4

1 回答 1

0

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

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

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

关于解析库和标记化,我真的不明白它与所讨论的问题有什么关系,但我可能错过了你的观点。此外,我不建议使用镜头来解决这样一个微不足道的问题,您最终可能会得到一个更复杂且不易维护的解决方案。

于 2021-11-04T12:39:18.327 回答