如何压缩这些重复的F#代码?

时间:2018-05-21 18:59:57

标签: f#

编辑:底部的可能解决方案

我正在做一些数据工作,我需要非常小心字符串长度,最终将在固定宽度的文本输出中发送,存储在有限大小的nvarchar字段等中。我希望有一个很好的严格输入这些而不是裸体的System.String类型。

假设我有一些这样的代码来代表这些,有一些有用的模块函数可以很好地与Result.map,Option.map等一起使用。

module String40 =
    let private MaxLength = 40
    type T = private T of string
    let create (s:string) = checkStringLength MaxLength s |> Result.map T
    let trustCreate (s:string) = checkStringLength MaxLength s  |> Result.okVal |> T
    let truncateCreate (s:string) = truncateStringToLength MaxLength s |> T
    let toString (T s) = s

    type T with
        member this.AsString = this |> toString


module String100 =
    let private MaxLength = 100
    type T = private T of string
    let create (s:string) = checkStringLength MaxLength s |> Result.map T
    let trustCreate (s:string) = checkStringLength MaxLength s  |> Result.okVal |> T
    let truncateCreate (s:string) = truncateStringToLength MaxLength s |> T
    let toString (T s) = s

    type T with
        member this.AsString = this |> toString

显然,这些几乎完全重复,每个块中只有模块名称和最大长度不同。

有哪些选项可以尝试减少重复性?我很想得到这样的东西:

type String40 = LengthLimitedString<40>
type String100 = LengthLimitedString<100>

tryToRetrieveString ()   // returns Result<string, ERRType>
|> Result.bind String40.create
  • T4代码生成似乎不是F#项目的选项
  • 类型提供程序对于这种简单的模板似乎有点过分,而且我可以说它们只能生成类,而不是模块。
  • 我知道Scott Wlaschin的constrained strings页面,但我最终在&#39;创建类型&#39;&#中的重复代码大致相同39;实现IWrappedString&#39;,&#39;创建一个公共构造函数&#39;他列出的步骤。

这些代码块相当短,只是为不同的字段长度复制/粘贴十几次不会是世界末日。但我觉得我错过了一种更简单的方法。

更新:

另一个注意事项是,使用这些类型的记录提供有关他们所携带的类型的信息非常重要:

type MyRecord = 
  {
    FirstName: String40;
    LastName: String100;
  }

并不像

type MyRecord = 
  {
    FirstName: LimitedString;
    LastName: LimitedString;
  }

<小时/> 托马斯&#39;回答,使用Depended Type Provider nuget库是一个非常好的解决方案,并且对于许多对其行为一直很好的人来说都是一个很好的解决方案。我觉得扩展和自定义会有点棘手,除非我想维护我自己希望避免的类型提供者的副本。

马塞洛关于静态参数约束的建议是一条相当富有成效的研究路径。他们基本上给了我我想要的东西 - 一个基本上是一个界面的通用论点&#39;对于静态方法。然而,问题是它们需要内联函数才能运行,而我没有时间来评估代码库中有多少重要或不重要。

但是我接受了并改变它以使用常规的通用约束。有必要实例化一个对象以获得最大长度值有点傻,而fsharp类型/通用代码只是很难看,但从模块用户的角度看它是干净的,我可以随心所欲地扩展它。

    type IMaxLengthProvider = abstract member GetMaxLength: unit -> int

    type MaxLength3 () = interface IMaxLengthProvider with member this.GetMaxLength () = 3
    type MaxLength4 () = interface IMaxLengthProvider with member this.GetMaxLength () = 4


    module LimitedString =

        type T< 'a when 'a :> IMaxLengthProvider> = private T of string

        let create< 't when 't :> IMaxLengthProvider and 't : (new:unit -> 't)> (s:string) =
            let len = (new 't()).GetMaxLength()
            match checkStringLength len s with
            | Ok s ->
                let x : T< 't> = s |> T
                x |> Ok
            | Error e -> Error e
        let trustCreate< 't when 't :> IMaxLengthProvider and 't : (new:unit -> 't)> (s:string) =
            let len = (new 't()).GetMaxLength()
            match checkStringLength len s with
            | Ok s ->
                let x : T< 't> = s |> T
                x
            | Error e -> 
                let msg = e |> formErrorMessage
                failwith msg

        let truncateCreate< 't when 't :> IMaxLengthProvider and 't : (new:unit -> 't)> (s:string) =
            let len = (new 't()).GetMaxLength()
            let s = truncateStringToLength len s
            let x : T< 't> = s |> T
            x

        let toString (T s) = s
        type T< 'a when 'a :> IMaxLengthProvider> with
            member this.AsString = this |> toString


    module test =
        let dotest () =

            let getString () = "asd" |> Ok

            let mystr = 
                getString ()
                |> Result.bind LimitedString.create<MaxLength3>
                |> Result.okVal
                |> LimitedString.toString

            sprintf "it is %s" mystr

4 个答案:

答案 0 :(得分:3)

我认为Dependent type provider项目中的BoundedString类型提供程序可让您完全满足您的需求。使用项目文档中的示例,您可以执行以下操作:

type ProductDescription = BoundedString<10, 2000>
type ProductName = BoundedString<5, 50>

type Product = { Name : ProductName; Description : ProductDescription }

let newProduct (name : string) (description : string) : Product option =
  match ProductName.TryCreate(name), ProductDescription.TryCreate(description) with
  | Some n, Some d -> { Name = n; Description = d }
  | _ -> None

我不知道有多少人在实践中使用这个项目,但它看起来很简单,它完全符合你的要求,所以值得一试。

答案 1 :(得分:3)

使用一丝细致的反射魔法,我们可以实现很多并获得一些非常好的类型。这样的事情怎么样?

module Strings =
    type [<AbstractClass>] Length(value: int) =
        member this.Value = value

    let getLengthInst<'L when 'L :> Length> : 'L =
        downcast typeof<'L>.GetConstructor([||]).Invoke([||])

    type LimitedString<'Length when 'Length :> Length> =
        private | LimitedString of maxLength: 'Length * value: string

        member this.Value =
            let (LimitedString(_, value)) = this in value
        member this.MaxLength =
            let (LimitedString(maxLength, _)) = this in maxLength.Value

    module LimitedString =
        let checkStringLength<'L when 'L :> Length> (str: string) =
            let maxLength = getLengthInst<'L>.Value
            if str.Length <= maxLength then Ok str
            else Error (sprintf "String of length %d exceeded max length of %d" str.Length maxLength)

        let create<'L when 'L :> Length> (str: string) =
            checkStringLength<'L> str
            |> Result.map (fun str -> LimitedString (getLengthInst<'L>, str))

open Strings

// === Usage ===

type Len5() = inherit Length(5)
type Len1() = inherit Length(1)

// Ok
LimitedString.create<Len5> "Hello"
// Error
LimitedString.create<Len1> "world"

答案 2 :(得分:2)

一个选项可能是将单个模块用于有限长度的字符串,该字符串使用curried参数作为长度限制和字符串本身,然后只部分应用limit参数。实现可能如下所示:

module LimitedString =
    type T = private T of string
    let create length (s:string) = checkStringLength length s |> Result.map T
    let trustCreate length (s:string) = checkStringLength length s  |> Result.okVal |> T
    let truncateCreate length (s:string) = truncateStringToLength length s |> T
    let toString (T s) = s

    type T with
        member this.AsString = this |> toString

然后,仍然需要每个长度的模块,但不会有所有样板:

module String100 =
    let create = LimitedString.create 100
    let trustCreate = LimitedString.trustCreate 100
    let truncateCreate = LimitedString.truncateCreate 100

修改

在阅读评论和原帖的更新后,我会稍微改变一下我的建议。我没有在每个模块中定义T类型,而是在顶层为每个字符串长度设置一个特定的struct-type单案例并集。然后,我将toString移动到各个字符串模块。最后,我将向LimitedString模块添加一个参数,以允许我们部分应用长度和特定的单例联合类型:

[<Struct>] type String40 = private String40 of string
[<Struct>] type String100 = private String100 of string

module LimitedString =
    let create length ctor (s:string) = checkStringLength length s |> Result.map ctor
    let trustCreate length ctor (s:string) = checkStringLength length s  |> Result.okVal |> ctor
    let truncateCreate length ctor (s:string) = truncateStringToLength length s |> ctor

module String40 =
    let create = LimitedString.create 40 String40
    let trustCreate = LimitedString.trustCreate 40 String40
    let truncateCreate = LimitedString.truncateCreate 40 String40
    let toString (String40 s) = s

module String100 =
    let create = LimitedString.create 100 String100
    let trustCreate = LimitedString.trustCreate 100 String100
    let truncateCreate = LimitedString.truncateCreate 100 String100
    let toString (String100 s) = s

type MyRecord =
    {
        FirstName: String40
        LastName: String100
    }

这里仍有相当数量的样板,但我认为这是使用单箱联合和模块的解决方案。类型提供程序可能是可能的,但您必须考虑增加的复杂性是否超过样板。

答案 3 :(得分:1)

你不能使用Static Parameters,如F#.Data包的HtmlProvider示例所示?