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.
Questo mi sembra simile alla "monade" Errore. Pensi che lo sia ma con il tipo 'TFailure' che è effettivamente l'equivalente di' Exception'? –
Enigmativity
@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. –
Il tipo 'RopValue' è una monade. Fondamentalmente le monadi danno ai tipi ordinari superpoteri. – Enigmativity