F#:使用特定类型/别名类型的经验法则?

时间:2019-05-31 10:56:05

标签: f#

我正在创建一种“信封”来跟踪通过应用程序中RabbitMQ发送的内容(命令和事件),如下所示:

type Envelope<'T> = {
    Id: Guid
    CausationId: Guid
    CorrelationId: Guid
    Payload: 'T
    Timestamp: DateTimeOffset
}

'T通常是以下形式的命令或事件:

type Commands =
    | DoA of Stuff
    | DoB of Stuff
    | DoC of Stuff

type Events =
    | AHappened of Stuff
    | BHappened of Stuff
    | CHappened of Stuff

注意:我故意使用相对通用的名称

但是,我也可以使用特定类型:

type CausationId = CausationId of Guid
type CorrelationId = CorrelationId of Guid
type CommandId = CommandId of Guid
type EventId = EventId of Guid
type Timestamp = Timestamp of DateTimeOffset

type DataToDoA = DataToDoA of Stuff
type DataToDoB = DataToDoB of Stuff
type DataToDoC = DataToDoC of Stuff

type Commands =
    | DoA of DataToDoA
    | DoB of DataToDoB
    | DoC of DataToDoC

type DataLeftByA = DataLeftByA of Stuff
type DataLeftByB = DataLeftByB of Stuff
type DataLeftByC = DataLeftByC of Stuff

type Events =
    | AHappened of DataLeftByA
    | BHappened of DataLeftByB
    | CHappened of DataLeftByC

这会导致:

type CommandEnvelope<`T> = {
    Id: CommandId
    CausationId: CausationId
    CorrelationId: CorrelationId
    Payload: `T
    Timestamp: Timestamp
}

type EventEnvelope<`T> = {
    Id: EventId
    CausationId: CausationId
    CorrelationId: CorrelationId
    Payload: `T
    Timestamp: Timestamp
}

是否有经验法则来决定何时使用这些特定类型/别名?

2 个答案:

答案 0 :(得分:2)

我认为这没有经验法则,但是一种系统的思考方式是考虑是否需要将那些额外类型表示的值自己传递给代码。如果否,那么您不需要那些额外的类型。如果是,那么它们可能会有用。

假设您只有Stuff并且:

type DataLeftByA = DataLeftByA of Stuff
type DataLeftByB = DataLeftByB of Stuff

type Events =
    | AHappened of DataLeftByA
    | BHappened of DataLeftByB

这可以让您做的一件事就是编写一个函数:

let processDataA (DataLeftByA stuff) = (...)

此函数需要DataLeftByA包含一些Stuff。但是,该类型清楚地表明该功能仅应用于由A引起的事件。以下将是类型错误:

let handleEvent = function
    | AHappened adata -> processDataA adata
    | BHappened bdata -> processDataA bdata // Type error here!

如果您将Events定义为包含相同Stuff的事件:

type Events =
    | AHappened of Stuff
    | BHappened of Stuff

然后,事件所携带的数据是相同的,但是您失去了定义诸如processDataA之类的功能的能力,因为事件A所携带的数据没有单独的类型。您可以只定义processStuff,但是可以在AB情况下调用它。

我认为这是在两个版本之间产生真正实际差异的唯一内容。因此,经验法则是-您是否需要定义一个像processDataA这样的函数?

答案 1 :(得分:1)

Scott Wlaschin of F# for Fun and Profit talks a bit about this in his "Designing with Types" series. In the post on Single Case Union Types, he explains that one of the main reasons for using single-case union-types is when you want to add some validation logic. This allows you to refine your type a bit more.

The example he gives is email. You could have type Person = { ... Email: string ... } but this does not provide any guarantee that the string is an email address. Making a newtype for it type EmailAddress = Email of string you can then you can only create email addresses from functions which validate it first. This can be a good sanity check and helps make sure you don't assign email addresses to things which aren't addresses/links.

TL;DR Single-case union types are useful for whenever you want to add further semantics/validation to your code.