Программирование на железной дороге в С# - Как написать функцию переключателя?

Я следил за этой статьей 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 (как и он)? Также как написать функцию переключателя, которая позволит мне подключить простые функции проверки?

Любые другие комментарии к моему коду приветствуются. Я не уверен, что это где-то рядом, как просто и просто, как могло бы быть.

Ответ 1

Функция Bind имеет неправильный тип, она должна быть:

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);
}

Параметр Func, переданный вашей реализации Bind, принимает параметр RopValue<TSuccess, TFailure>, а не только TSuccess. Это означает, что функция должна повторять то же соответствие на входе, которое метод Bind должен сделать для вас.

Это может быть немного громоздким из-за количества параметров типа, поэтому вы можете переместить его в базовый класс:

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);
    }
}

Затем вы можете избежать создания фиктивного значения в начале цепочки:

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

Ответ 2

Ли верно, что ваша функция привязки определена неверно.

Привязка всегда должна иметь подпись типа, которая выглядит так: m<'a> -> ('a -> m<'b>) -> m<'b>

Я определил его так, но Ли функционально идентичен:

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 Composition (>=>) имеет подпись типа, которая выглядит так: ('a -> m<'b>) -> ('b -> m<'c>) -> 'a -> m<'c>

Вы можете определить, что с помощью 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));
}

Вы можете определить методы расширения на Func, но трюк заставит компилятор увидеть, что эти методы расширения доступны, что-то вроде этого будет работать:

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

Где request - ваши данные для проверки.

Обратите внимание, что создавая переменную типа Func, она позволяет использовать метод расширения.