EDITAR: posibles soluciones en la parte inferior

Estoy haciendo un trabajo de datos en el que necesito tener mucho cuidado con las longitudes de cadena que eventualmente se enviarán en texto de ancho fijo, almacenados en campos nvarchar de tamaño limitado, etc. Quiero tener un tipeo estricto para estos en lugar de un sistema simple Tipos de cuerda.

Supongamos que tengo un código como este para representarlos, con algunas funciones de módulos útiles que funcionan bien con Result.map, Option.map, etc.

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

Obviamente, estos son casi completamente repetitivos con solo el nombre del módulo y la longitud máxima diferentes en cada bloque.

¿Qué opciones hay disponibles para tratar de reducir la repetitividad aquí? Me encantaría tener algo como esto:

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

tryToRetrieveString ()   // returns Result<string, ERRType>
|> Result.bind String40.create
  • La generación de código T4 no parece ser una opción para proyectos de F #
  • Los proveedores de tipos parecen exagerados para este tipo de plantillas simples, y lo mejor que puedo decir es que solo pueden producir clases, no módulos.
  • Soy consciente de la página de cadenas restringidas de Scott Wlaschin. pero termino con aproximadamente el mismo nivel de código repetitivo en los pasos 'Crear un tipo', 'Implementar IWrappedString', 'crear un constructor público' que enumera.

Estos bloques de código son bastante cortos y no sería el fin del mundo simplemente copiar / pegar una docena de veces para las diferentes longitudes de campo. Pero siento que me falta una forma más simple de hacer esto.

Actualizar:

Otra nota es que es importante que los registros que usan estos tipos brinden información sobre el tipo que llevan:

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

Y no ser algo como

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

La sugerencia de Marcelo de restricciones de parámetros estáticos fue un camino de investigación bastante productivo. Básicamente me dan lo que estaba buscando: un argumento genérico que es básicamente una 'interfaz' para métodos estáticos. Sin embargo, lo importante es que requieren funciones en línea para operar y no tengo tiempo para evaluar cuánto importaría o no en mi base de código.

Pero tomé eso y lo alteré para usar restricciones genéricas regulares. Es un poco tonto tener que crear una instancia de un objeto para obtener un valor de longitud máxima, y el tipo fsharp / código genérico es simplemente asqueroso, pero desde la perspectiva de los usuarios del módulo está limpio, y puedo extenderlo fácilmente como quiera.

    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
f#
2
Clyde 21 may. 2018 a las 21:59

4 respuestas

La mejor respuesta

¿No puede usar Estático Parámetros, como se muestra en el HtmlProvider del paquete F # .Data.

1
user4649737user4649737 21 may. 2018 a las 19:40

Creo que el proveedor de tipo BoundedString del El proveedor de tipo dependiente le permite hacerlo exactamente lo que necesitas Usando el ejemplo de la documentación del proyecto, puede hacer, por ejemplo:

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

No sé cuántas personas usan este proyecto en la práctica, pero parece bastante simple y hace exactamente lo que estaba pidiendo, por lo que podría valer la pena intentarlo.

3
Tomas Petricek 21 may. 2018 a las 20:33

Con un toque de cuidadosa magia de reflexión, podemos lograr mucho y obtener algunos tipos realmente agradables. ¿Qué tal algo como esto?

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"
3
Jwosty 21 may. 2018 a las 21:29

Una opción podría ser usar un solo módulo para cadenas de longitud limitada que utiliza parámetros curriculares para el límite de longitud y la cadena en sí misma, luego simplemente aplicar parcialmente el parámetro de límite. Una implementación podría verse así:

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

Entonces, sus módulos para cada longitud aún serían necesarios, pero no tendrían toda la plantilla:

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

EDITAR

Después de leer el comentario y la actualización de la publicación original, cambiaría un poco mi sugerencia. En lugar de definir el tipo T dentro de cada módulo, tendría una unión de caso único de tipo estructura específica para cada longitud de cadena en el nivel superior. Luego, me movería a toString a los módulos de cadena individuales. Finalmente, agregaría un parámetro más al módulo LimitedString para permitirnos aplicar parcialmente tanto la longitud como el tipo de unión de caso único específico:

[<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
    }

Todavía hay una buena cantidad de repeticiones aquí, pero creo que esto está en el campo de juego para una solución usando uniones y módulos de caso único. Un proveedor de tipos podría ser posible, pero tendría que considerar si la complejidad agregada supera a la repetitiva.

2
Aaron M. Eshbach 21 may. 2018 a las 20:03