database - F# 如何展平与其他类型混合的嵌套结果类型

标签 database f# domain-driven-design

我目前正在开发领域驱动设计项目的方法存储库。

从存储库获取 DTO 的方法返回以下类型:Async<Result<DTO.Worker option, exn>> ,然后运行以下代码将该 DTO 转换为 Domain.Worker :

methodResult
|> Async.map ((Result.map (Option.map DTO.Worker.toDomain)))

Domain.Worker记录类型有一个可区分的联合作为其值之一,我们不能直接在数据库中使用它们,因此在域模型之外有一个额外的步骤,将可区分的联合映射到数字,以便我们可以使用将其保存到数据库中.

module Domain

type WorkerStatus =
| Status1
| Status2
| Status3

type Worker = {
    ...
    Status : WorkerStatus
}

问题在于 DTO.Worker.toDomain我使用模式匹配将这些数字映射回可识别联合的方法:

module DTO

type Worker = {...}
with
    static member toDomain (dbo : Worker) : Domain.Worker = {
            ...
            Domain.Worker.Status =
                match dbo.Status with
                | 1 -> Domain.WorkerStatus.Status1
                | 2 -> Domain.WorkerStatus.Status2
                | 3 -> Domain.WorkerStatus.Status3
                | _ -> raise (exn "data in the database has the wrong format")
        }

在我们启动项目时,我可以避免引发异常,但现在我正在清理代码,我也需要将此方法包装在结果中,这引发了两个问题:

  1. 如何按照 F# 标准将代码包装在结果中?我仍在学习 F#,所以我能想到的唯一方法是在 toDomain 的开头创建一个可变的 bool 变量方法,如果我们在模式匹配中遇到任何错误,则更改它,然后返回转换为域模型的 DTO 或基于该 bool 值的错误。我还尝试用 try...with... 包围该方法阻止并离开 raise exn ,但我的同事也说这不是一个好的解决方案。
  2. 如何展平新的嵌套结果类型?如果我们更改代码,则 toDomain方法返回 Result 而不是 Domain 对象,我们最终会得到 Async<Result<Result<Domain.Worker, exn> option, exn>>类型,这并不像我们期望的那样有用 Async<Result<Domain.Worker option, exn>>代码中的其他地方。

最佳答案

短期建议

我认为您已经将自己陷入了这些类型签名的困境中,因此没有简单的解决方案,但这是我能想到的侵入性最小的方法,这似乎符合您的要求。

首先修改toDomain所以它返回 Result而不是抛出异常:

        static member toDomain (dbo : Worker) =
            let statusResult =
                match dbo.Status with
                | 1 -> Ok Domain.WorkerStatus.Status1
                | 2 -> Ok Domain.WorkerStatus.Status2
                | 3 -> Ok Domain.WorkerStatus.Status3
                | _ -> Error (exn "data in the database has the wrong format")
            statusResult
                |> Result.map (fun status ->
                {
                    //...
                    Domain.Worker.Status = status
                })

现在,正如您所提到的,问题是您有一个 Result<Option<Result<'a, exn>>, exn>当您想要Result<Option<'a>, exn>时,所以我们需要一个转换函数:

let convert = function
    | Ok (Some (Ok x)) -> Ok (Some x)
    | Ok (Some (Error exn)) -> Error exn
    | Ok None -> Ok None
    | Error exn -> Error exn

当您将 DTO 转换为域值时,您需要:

methodResult
|> Async.map ((Result.map (Option.map DTO.Worker.toDomain)) >> convert)

我并不是说这是一个好主意,但它至少应该让你暂时解开这个结。

长期建议

我认为混合是不明智的ResultOption按照您现有的方式键入。如果我正确理解您的设计,则值为 Ok None当前表示数据访问层执行成功,但没有找到请求的对象。我明白为什么你最终会采用这种设计,但我认为这比它的值(value)更麻烦。相反,我建议每个操作都返回类似 Result<'a, Error> 的内容。相反,Error类型可以是:

type Error = Exn of exn | NotFound

例如,Ok None然后将变成Error NotFound反而。一旦进行了更改,您就会停留在单个 Result monad 内,并且 F# 可以让您的生活变得更加轻松。例如,您可以使用 computation expression像这样:

let getDomainResult queryParams =
    result {
        let! dto = getDto queryParams
        return! convertToDomain dto
    }

(注意:为了简单起见,我在这里忽略了 Async,但是 AsyncResult 可能更适合您。无论哪种情况,只要远离同一工作流程中的 Option 类型即可。您无能为力使用 Option 无法使用 Result 完成。)

关于database - F# 如何展平与其他类型混合的嵌套结果类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/74033474/

相关文章:

python - 如何使用 SQLAlchemy 获取列值?

F# : getting rows/column names 的 JSON 类型提供程序

c# - 使用微服务过程中的步骤向导 UI

c# - DTO 应该由域实体生成还是由持久性生成?

mysql - SQL-单个字段上的多项选择

php - 没有数据库存储数据?

java - 检查数据库是否存在

f# - 来自其他 .NET 语言的重载运算符

F# Excel Range.AutoFilter() 失败

c# - 使用未找到的实体调用服务操作