Я следил за этой статьей F # ROP и решил попробовать и воспроизвести ее на С#, в основном, чтобы посмотреть, могу ли я это сделать. Извиняюсь за длину этого вопроса, но если вы знакомы с ROP, это будет очень легко следовать.
Он начал с разграниченного объединения F #...
type Result<'TSuccess, 'TFailure> =
| Success of 'TSuccess
| Failure of 'TFailure
... который я перевел в абстрактный класс RopValue и две конкретные реализации (обратите внимание, что я изменил имена классов на те, которые я понял лучше)...
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; }
}
Я добавил статический метод Create, который позволит вам создать RopValue из объекта TSuccess, который будет передан в первую из функций проверки.
Затем я начал писать функцию привязки. Версия F # была следующей:
let bind switchFunction twoTrackInput =
match twoTrackInput with
| Success s -> switchFunction s
| Failure f -> Failure f
... который был поводом для чтения по сравнению с эквивалентом С#! Я не знаю, есть ли более простой способ написать это, но вот что я придумал...
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;
}
Обратите внимание, что я написал это как функцию расширения, так как это позволило мне использовать его более функциональным способом.
Взяв пример использования человека, я написал класс Person...
public class Person {
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
}
... и написал мою первую функцию проверки...
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;
}
С помощью нескольких аналогичных проверок для электронной почты и возраста я мог бы написать общую функцию проверки следующим образом:
private static RopValue<Person, string> Validate(Person person) {
return RopValue<Person, string>
.Create<Person, string>(person)
.Bind(CheckName)
.Bind(CheckEmail)
.Bind(CheckAge);
}
Это прекрасно работает и позволяет мне делать что-то вроде этого...
Person jim = new Person {Name = "Jim", Email = "", Age = 16};
RopValue<Person, string> jimChk = Validate(jim);
Debug.WriteLine("Jim returned: " + (jimChk.IsSuccess() ? "Success" : "Failure"));
Однако у меня есть несколько проблем с тем, как я это сделал. Во-первых, функции проверки требуют, чтобы вы проходили в RopValue, проверяете его на "Успех" или "Неудача", если "Успех", вытащите Личность и затем подтвердите его. Если Failure, просто верните его.
В отличие от него, его функции проверки принимали (эквивалент) Person и возвращались (результат, являющийся эквивалентом) RopValue...
let validateNameNotBlank person =
if person.Name = "" then Failure "Name must not be blank"
else Success person
Это намного проще, но я не смог решить, как это сделать на С#.
Другая проблема заключается в том, что мы начинаем цепочку валидации с помощью Success < > , поэтому первая функция проверки всегда будет возвращать что-то из блока if, либо Failure < > если проверка не выполнена, либо Success < > if мы прошли проверку. Если функция возвращает Failure < > , то следующая функция в цепочке валидации никогда не будет вызвана, поэтому выясняется, что мы знаем, что эти методы никогда не могут быть переданы Failure < > . Поэтому конечная строка каждой из этих функций никогда не будет достигнута (кроме как в странном случае, когда вы вручную создали Failure < > и передали его в начале, но это было бы бессмысленно).
Затем он создал оператор переключения ( >= > ) для подключения функций проверки. Я пытался это сделать, но не мог заставить его работать. Чтобы связать последовательные вызовы функции, казалось, что мне нужно иметь метод расширения на Func < > , который, как я думаю, вы не можете сделать. Я дошел до этого...
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);
}
... но не смог понять, как его использовать.
Итак, может ли кто-нибудь объяснить, как я напишу функцию Bind, чтобы она могла взять Person и вернуть RopValue (как и он)? Также как написать функцию переключателя, которая позволит мне подключить простые функции проверки?
Любые другие комментарии к моему коду приветствуются. Я не уверен, что это где-то рядом, как просто и просто, как могло бы быть.