2016-03-10 11 views
11

Ho seguito this F# ROP article e ho deciso di provare a riprodurlo in C#, principalmente per vedere se potevo. Ci scusiamo per la lunghezza di questa domanda, ma se hai familiarità con ROP, sarà molto facile da seguire.Programmazione ferroviaria orientata in C# - Come si scrive la funzione dell'interruttore?

Ha iniziato con un F # discriminati unione ...

type Result<'TSuccess, 'TFailure> = 
    | Success of 'TSuccess 
    | Failure of 'TFailure 

... che ho tradotto in una classe RopValue astratta, e due implementazioni concrete (nota che ho cambiato i nomi di classe a quelli che ho capito meglio) ...

public abstract class RopValue<TSuccess, TFailure> { 
    public static RopValue<TSuccess, TFailure> Create<TSuccess, TFailure>(TSuccess input) { 
    return new Success<TSuccess, TFailure>(input); 
    } 
} 

public class Success<TSuccess, TFailure> : RopValue<TSuccess, TFailure> { 
    public Success(TSuccess value) { 
    Value = value; 
    } 
    public TSuccess Value { get; set; } 
} 

public class Failure<TSuccess, TFailure> : RopValue<TSuccess, TFailure> { 
    public Failure(TFailure value) { 
    Value = value; 
    } 
    public TFailure Value { get; set; } 
} 

ho aggiunto un metodo Create statico per consentire di creare un RopValue da un oggetto TSuccess, che verrebbe immessa nella prima delle funzioni di validazione.

Ho quindi continuato a scrivere una funzione vincolante. La versione F # era la seguente ...

let bind switchFunction twoTrackInput = 
    match twoTrackInput with 
    | Success s -> switchFunction s 
    | Failure f -> Failure f 

... che era un gioco da leggere rispetto all'equivalente C#! Non so se c'è un modo più semplice per scrivere questo, ma qui è quello che mi è venuta ...

public static RopValue<TSuccess, TFailure> Bind<TSuccess, TFailure>(this RopValue<TSuccess, TFailure> input, Func<RopValue<TSuccess, TFailure>, RopValue<TSuccess, TFailure>> switchFunction) { 
    if (input is Success<TSuccess, TFailure>) { 
    return switchFunction(input); 
    } 
    return input; 
} 

nota che ho scritto questo come una funzione di estensione, come che mi ha permesso di usarlo in un modo più funzionale.

Prendendo il suo caso uso di convalida di una persona, poi ho scritto una classe Person ...

public class Person { 
    public string Name { get; set; } 
    public string Email { get; set; } 
    public int Age { get; set; } 
} 

... e scritto il mio primo funzione di convalida ...

public static RopValue<Person, string> CheckName(RopValue<Person, string> res) { 
    if (res.IsSuccess()) { 
    Person person = ((Success<Person, string>)res).Value; 
    if (string.IsNullOrWhiteSpace(person.Name)) { 
     return new Failure<Person, string>("No name"); 
    } 
    return res; 
    } 
    return res; 
} 

Con una un paio di convalide simili per e-mail ed età, potrei scrivere una funzione di validazione generale come segue ...

private static RopValue<Person, string> Validate(Person person) { 
    return RopValue<Person, string> 
    .Create<Person, string>(person) 
    .Bind(CheckName) 
    .Bind(CheckEmail) 
    .Bind(CheckAge); 
} 

Questo funziona bene, e mi permette di fare qualcosa di simile ...

Person jim = new Person {Name = "Jim", Email = "", Age = 16}; 
RopValue<Person, string> jimChk = Validate(jim); 
Debug.WriteLine("Jim returned: " + (jimChk.IsSuccess() ? "Success" : "Failure")); 

Tuttavia, ho un paio di problemi con il modo in cui ho fatto questo. Prima di tutto è che le funzioni di convalida richiedono di passare un valore RopValue, controllarlo in caso di esito positivo o negativo, in caso di successo, estrarre la persona e quindi convalidarla. Se Failure, basta restituirlo.

Al contrario, le sue funzioni di validazione ha preso (l'equivalente di) una persona, e restituito (un risultato, che è l'equivalente di) un RopValue ...

let validateNameNotBlank person = 
    if person.Name = "" then Failure "Name must not be blank" 
    else Success person 

Questo è molto più semplice, ma io non è stato in grado di capire come farlo in C#.

Un altro problema è che iniziamo la catena di validazione con un successo <>, quindi la prima funzione di convalida sarà sempre restituire qualcosa dal "se" blocco, o un guasto <> Se la convalida non è riuscita, o un successo <> se abbiamo superato i controlli. Se una funzione restituisce Failure <>, la funzione successiva nella catena di convalida non viene mai chiamata, quindi risulta che sappiamo che questi metodi non possono mai essere passati a un errore <>.Pertanto, la riga finale di ciascuna di queste funzioni non può mai essere raggiunta (tranne nel caso strano in cui è stato creato manualmente un errore <> e lo ha inoltrato all'inizio, ma ciò sarebbe inutile).

Ha quindi creato un operatore di commutazione (> =>) per collegare le funzioni di convalida. Ho provato a farlo, ma non ho potuto farlo funzionare. Al fine di concatenare chiamate successive alla funzione, sembrava che avrei dovuto avere un metodo di estensione su un Func <>, che non credo tu possa fare. Mi sono spinto fino a questo ...

public static RopValue<TSuccess, TFailure> Switch<TSuccess, TFailure>(this Func<TSuccess, RopValue<TSuccess, TFailure>> switch1, Func<TSuccess, RopValue<TSuccess, TFailure>> switch2, TSuccess input) { 
    RopValue<TSuccess, TFailure> res1 = switch1(input); 
    if (res1.IsSuccess()) { 
    return switch2(((Success<TSuccess, TFailure>)res1).Value); 
    } 
    return new Failure<TSuccess, TFailure>(((Failure<TSuccess, TFailure>)res1).Value); 
} 

... ma non riuscivo a capire come usarlo.

Quindi, qualcuno può spiegare come scrivere la funzione Bind in modo che possa prendere una Persona e restituire un RopValue (come il suo)? Inoltre, come posso scrivere una funzione di commutazione che mi consenta di collegare semplici funzioni di convalida?

Qualsiasi altro commento sul mio codice è benvenuto. Non sono sicuro che sia ovunque così pulito e semplice come potrebbe essere.

+0

Questo mi sembra simile alla "monade" Errore . Pensi che lo sia ma con il tipo 'TFailure' che è effettivamente l'equivalente di' Exception'? – Enigmativity

+0

@Enigmativity Per citare Mark Twain, "Sono stato contento di poter rispondere rapidamente, ho detto che non lo sapevo!" - Sono un programmatore C#, sto imparando F # e la programmazione funzionale allo stesso tempo. Ho visto le monadi discusse, ma non ho ancora capito cosa siano. –

+0

Il tipo 'RopValue' è una monade. Fondamentalmente le monadi danno ai tipi ordinari superpoteri. – Enigmativity

risposta

4

La funzione Bind ha il tipo sbagliato, dovrebbe essere:

public static RopValue<TOut, TFailure> Bind<TSuccess, TFailure, TOut>(this RopValue<TSuccess, TFailure> input, Func<TSuccess, RopValue<TOut, TFailure>> switchFunction) { 
    if (input is Success<TSuccess, TFailure>) { 
    return switchFunction(((Success<TSuccess, TFailure>)input).Value); 
    } 
    return new Failure<TOut, TFailure>(((Failure<TSuccess, TFailure>)input).Value); 
} 

Il parametro Func passato a l'implementazione di Bind prende un parametro RopValue<TSuccess, TFailure> piuttosto che solo TSuccess. Ciò significa che la funzione deve ripetere la stessa corrispondenza sull'input che il metodo Bind dovrebbe fare per te.

Questo può essere un po 'ingombrante a causa del numero di parametri di tipo così si potrebbe spostarlo nella classe base:

public abstract RopValue<TOut, TFailure> Bind<TOut>(Func<TSuccess, RopValue<TOut, TFailure> f); 

public class Success<TSuccess, TFailure> : RopValue<TSuccess, TFailure> { 
    public override RopValue<TOut, TFailure> Bind<TOut>(Func<TSuccess, RopValue<TOut, TFailure> f) { 
     return f(this.Value); 
    } 
} 

public class Failure<TSuccess, TFailure> : RopValue<TSuccess, TFailure> { 
    public override RopValue<TOut, TFailure> Bind<TOut>(Func<TSuccess, RopValue<TOut, TFailure> f) { 
     return new Failure<TOut, TFailure>(this.Value); 
    } 
} 

si può quindi evitare di creare un valore fittizio all'inizio della catena:

private static RopValue<Person, string> Validate(Person person) { 
    return CheckName(person) 
    .Bind(CheckEmail) 
    .Bind(CheckAge); 
} 
+0

Avevo un'opportunità per guardarlo ora. Quando dici che la mia funzione Bind aveva il tipo sbagliato, per quanto posso vedere, l'unica vera differenza è che hai corretto la mia semplificazione subconscia assumendo che il tipo di input e output fosse lo stesso. Non sono sicuro del perché questo cambierebbe il modo in cui sei stato in grado di usarlo nel mio caso, dato che sto lavorando con una classe Person fino in fondo. Potresti spiegarlo un po 'di più? –

+0

Inoltre, ho provato a spostare la funzione Bind alla classe base, che andava bene per Success, poiché ho appena restituito switchFunction (Value), ma non riuscivo a capire cosa inserire nella classe Failure. Il mio pensiero era di restituire "questo" (essendo l'oggetto Failure stesso, che è ovviamente anche un oggetto RopValue), ma questo ha dato a un errore del compilatore che non poteva convertire un Failure a un RopValue. Anche il casting non ha aiutato. Cosa mi sono perso qui? Grazie ancora –

+0

@AvrohomYisroel - Vedi l'aggiornamento. – Lee

1

Lee è corretto che la funzione di collegamento è definita in modo errato.

Bind deve sempre avere una firma tipo che assomiglia: m<'a> -> ('a -> m<'b>) -> m<'b>

ho definito come questo, ma Lee è funzionalmente identico:

public static RopValue<TSuccess2, TFailure> Bind<TSuccess, TSuccess2, TFailure>(this RopValue<TSuccess, TFailure> input, Func<TSuccess, RopValue<TSuccess2, TFailure>> switchFunction) 
{ 
    if (input.IsSuccess) 
    { 
     return switchFunction(((Success<TSuccess,TFailure>)input).Value); 
    } 
    return new Failure<TSuccess2, TFailure>(((Failure<TSuccess, TFailure>)input).Value); 
} 

Kleisli Composizione (>=>) è di tipo firma che appare come segue: ('a -> m<'b>) -> ('b -> m<'c>) -> 'a -> m<'c>

È possibile definire che l'utilizzo di bind:

public static Func<TSuccess, RopValue<TSuccess2, TFailure>> Kleisli<TSuccess, TSuccess2, TFailure>(this Func<TSuccess, RopValue<TSuccess, TFailure>> switch1, Func<TSuccess, RopValue<TSuccess2, TFailure>> switch2) 
{ 
    return (inp => switch1(inp).Bind(switch2)); 
} 

È possibile definire metodi di estensione sul Func ma il trucco è sempre il compilatore di vedere che questi metodi di estensione sono disponibili, qualcosa di simile a questo dovrebbe funzionare:

Func<Entry, RopValue<Request, string>> checkEmail = CheckEmail; 
var combined = checkEmail.Kleisli(CheckAge); 
RopValue<Request, string> result = combined(request); 

Dove request è i dati per convalidare.

Si noti che creando una variabile di tipo Func, ci consente di utilizzare il metodo di estensione.

+0

La mia risposta permette di cambiare il tipo di successo, ecco a cosa serve il parametro 'TOut'. – Lee

+0

@Lee Hai ragione, mi scuso, ovviamente sto diventando matto. – TheInnerLight