实现依赖于部分记录类型的惯用方法是什么?

时间:2018-02-04 08:56:00

标签: f#

我的目的是定义一个具有函数的模块,这些函数可以对符合某些键的假设的所有记录类型进行操作。

为了说明,我们有以下代码:

> type DBRow = { id: string ; createdAt: System.DateTime } ;;
type DBRow =
  {id: string;
   createdAt: System.DateTime;}

> let logCreationInfo row = printf "Record %s created at %s " row.id (row.createdAt.ToString()) ;;
val logCreationInfo : row:DBRow -> unit

我想更改上面的logCreationInfo,以便能够对包含id: stringcreatedAt: System.DateTime(以及其他内容)的所有记录进行操作。

来自打字稿的结构打字,我预计这是微不足道的,但我正在探索在F#中有一种更惯用的处理方法。

我有attempted使用接口来处理这个问题,但即使这样可行,因为F#只支持显式接口,这不适用于我自己定义的类型。

3 个答案:

答案 0 :(得分:4)

您可以使用statically resolved type constraints

let inline logCreationInfo (x : ^t) =
    printfn "Record %s created at %s" 
        (^t : (member id : string) (x))
        ((^t : (member createdAt : System.DateTime) (x)).ToString())

答案 1 :(得分:4)

F#主要使用主格类型 - 这是其运行时环境中的自然选择,因为这是Common Type System规范规定的内容。遵守这套规则允许F#代码与其他.NET语言几乎无缝地互操作。

值得注意的是,这与TypeScript使用结构类型的原因相同。由于该语言建立在动态类型的JavaScript之上,因此根据结构而不是名义类型表达对象关系更为自然 - 这是JS中的一个外国概念。

F#确实有一个"后门"通过已经提到的SRTP进行结构分类,但我建议非常谨慎地使用它。 SRTP已经解析,使用它们的代码由编译器内联,缩短了编译时间,降低了与其他语言和.NET平台的互操作性(简单地说,您不能从其他语言引用该代码或使用反射API,因为它被编译掉了#34;)。

通常还有其他可用解决方案。已经提到了接口,尽管使用的示例有点人为 - 这更简单:

type IDBRow =
    abstract Id: string
    abstract CreatedAt: System.DateTime

type Person = 
    {
        id: string
        name: string
        age: int    
        createdAt: System.DateTime
    }
    interface IDBRow with
        member this.Id = this.id
        member this.CreatedAt = this.createdAt

let logCreationInfo (row: #IDBRow) = 
    printf "Record %s created at %s" row.Id (string row.CreatedAt)

let x = { id = "1"; name = "Bob"; age = 32; createdAt = DateTime.Now }
logCreationInfo x

或者使用合成和泛型类型来捕获DBRow的通用部分:

type DBRow<'data> = 
    {
        id: string
        data: 'data
        createdAt: System.DateTime
    }

type Person = 
    {
        name: string
        age: int    
    }

let logCreationInfo (row: DBRow<_>) = 
    printf "Record %s created at %s" row.id (string row.createdAt)

let x = { id = "1"; data = { name = "Bob"; age = 32 }; createdAt = DateTime.Now }
logCreationInfo x

答案 2 :(得分:1)

这是一个带接口的版本:

open System

type DBRow1 = {
    id: string
    createdAt: DateTime
}

type DBRow2 = {
    id: string
    createdAt: DateTime
    address: string
}

/// The types are defined above without an interface

let row1 = {id = "Row1"; createdAt = DateTime.Now}
let row2 = {id = "Row2"; createdAt = DateTime.Now; address = "NYC"}

type IDBRow<'A> =
    abstract member Data:(string * DateTime) 

// Object expression implements the interface
let Data1 (x:DBRow1)  = {
    new IDBRow<_> with
        member __.Data = (x.id, x.createdAt)
}

let Data2 (x: DBRow2) = {
    new IDBRow<_> with
    member __.Data = (x.id, x.createdAt)
}

//pass in both the object expression and the record
let getData (ifun: 'a -> IDBRow<'b>) xrec = 
    (ifun xrec).Data

// You could partially apply the functions: `getData1 = getData Data1` 
getData Data1 row1 //("Row1", 2018/02/05 9:24:17)
getData Data2 row2 //("Row2", 2018/02/05 9:24:17)

即使您无法访问原始类型,也可以使用接口(在这种情况下为对象表达式)来粘贴其他成员.Data。您仍然需要为每种类型组合一个对象表达式,因此SRTP可能是一个更“优雅”的解决方案。