Подтвердить что ты не робот

Каков самый простой способ доступа к данным типа F # дискриминационного объединения в С#?

Я пытаюсь понять, насколько хорошо С# и F # могут играть вместе. Я взял код из F # для блога Fun and Profit, который выполняет базовую проверку, возвращая дискриминированный тип объединения:

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

type Request = {name:string; email:string}

let TestValidate input =
    if input.name = "" then Failure "Name must not be blank"
    else Success input

При попытке использовать это в С#; единственный способ найти доступ к значениям против успеха и неудачи (неудача - это строка, успех - запрос снова) - с большими неприятными отбрасываниями (что очень много печатает и требует ввода фактических типов, которые я ожидал бы выводятся или доступны в метаданных):

var req = new DannyTest.Request("Danny", "fsfs");
var res = FSharpLib.DannyTest.TestValidate(req);

if (res.IsSuccess)
{
    Console.WriteLine("Success");
    var result = ((DannyTest.Result<DannyTest.Request, string>.Success)res).Item;
    // Result is the Request (as returned for Success)
    Console.WriteLine(result.email);
    Console.WriteLine(result.name);
}

if (res.IsFailure)
{
    Console.WriteLine("Failure");
    var result = ((DannyTest.Result<DannyTest.Request, string>.Failure)res).Item;
    // Result is a string (as returned for Failure)
    Console.WriteLine(result);
}

Есть ли лучший способ сделать это? Даже если мне придется вручную (с возможностью ошибки во время выполнения), я надеюсь, по крайней мере, сократить доступ к типам (DannyTest.Result<DannyTest.Request, string>.Failure). Есть ли лучший способ?

4b9b3361

Ответ 1

Работа с дискриминационными профсоюзами никогда не будет столь же понятной на языке, который не поддерживает сопоставление шаблонов. Тем не менее, ваш тип Result<'TSuccess, 'TFailure> достаточно прост, что должен быть хороший способ использовать его из С# (если тип был чем-то более сложным, например, деревом выражений, то я бы предположил использовать шаблон Visitor).

Другие уже упомянули несколько опций - как получить доступ к значениям напрямую, так и как определить метод Match (как описано в блоге Mauricio). Мой любимый метод простых DU - это определить методы TryGetXyz, которые следуют одному и тому же стилю Int32.TryParse - это также гарантирует, что разработчики С# будут знакомы с шаблоном. Определение F # выглядит следующим образом:

open System.Runtime.InteropServices

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

type Result<'TSuccess, 'TFailure> with
  member x.TryGetSuccess([<Out>] success:byref<'TSuccess>) =
    match x with
    | Success value -> success <- value; true
    | _ -> false
  member x.TryGetFailure([<Out>] failure:byref<'TFailure>) =
    match x with
    | Failure value -> failure <- value; true
    | _ -> false

Это просто добавляет расширения TryGetSuccess и TryGetFailure, которые возвращают true, когда значение соответствует параметрам case и return (all) случая дискриминированного объединения через параметры out. Использование С# довольно просто для любого, кто когда-либо использовал TryParse:

  int succ;
  string fail;

  if (res.TryGetSuccess(out succ)) {
    Console.WriteLine("Success: {0}", succ);
  }
  else if (res.TryGetFailure(out fail)) {
    Console.WriteLine("Failuere: {0}", fail);
  }

Я думаю, что знакомство с этим образцом является самым важным преимуществом. Когда вы используете F # и выставляете свой тип разработчикам С#, вы должны раскрывать их самым прямым способом (пользователям С# не следует думать, что типы, определенные в F #, не являются стандартными).

Кроме того, это дает вам разумные гарантии (когда оно используется правильно), что вы получите доступ только к значениям, которые действительно доступны, когда DU соответствует конкретному случаю.

Ответ 2

Mauricio Scheffer сделал несколько отличных сообщений для взаимодействия С#/F # и использования методов, как с основными библиотеками F # (или с библиотеками Fsharpx), так и без них, таким образом, чтобы иметь возможность использовать понятия (простые в F #) в С#.

http://bugsquash.blogspot.co.uk/2012/03/algebraic-data-type-interop-f-c.html

http://bugsquash.blogspot.co.uk/2012/01/encoding-algebraic-data-types-in-c.html

Также это может быть полезно: Как я могу дублировать тип объединенного типа F # в С#?

Ответ 3

Действительно хороший способ сделать это с помощью С# 7.0 - использовать сопоставление с шаблоном коммутатора, это почти как F # match:

var result = someFSharpClass.SomeFSharpResultReturningMethod()

switch (result)
{
    case var checkResult when checkResult.IsOk:
       HandleOk(checkResult.ResultValue);
       break;
    case var checkResult when checkResult.IsError:
       HandleError(checkResult.ErrorValue);
       break;
}

РЕДАКТИРОВАТЬ: С# 8.0 не за горами, и он приносит выражения переключения, поэтому, хотя я еще не пробовал это, я ожидаю, что мы сможем сделать что-то вроде этого:

var returnValue = result switch 
{
    var checkResult when checkResult.IsOk:     => HandleOk(checkResult.ResultValue),
    var checkResult when checkResult.IsError   => HandleError(checkResult.ErrorValue),
    _                                          => throw new UnknownResultException()
};

См. Https://blogs.msdn.microsoft.com/dotnet/2018/11/12/building-c-8-0/ для получения дополнительной информации.

Ответ 4

Возможно, одним из простейших способов добиться этого является создание набора методов расширения:

public static Result<Request, string>.Success AsSuccess(this Result<Request, string> res) {
    return (Result<Request, string>.Success)res;
}

// And then use it
var successData = res.AsSuccess().Item;

Эта статья содержит хорошее представление. Цитата:

Преимущество этого подхода в 2 раза:

  • Устраняет необходимость явного указания типов в коде и, следовательно, возвращает преимущества вывода типа;
  • Теперь я могу использовать . для любого из значений и позволить Intellisense помочь мне найти подходящий метод для использования;

Единственным недостатком здесь является то, что измененный интерфейс потребует рефакторинга методов расширения.

Если в вашем проекте (проектах) слишком много таких классов, рассмотрите возможность использования таких инструментов, как ReSharper, поскольку для этого не сложно создать генерацию кода.

Ответ 5

У меня была такая же проблема с типом результата. Я создал новый тип ResultInterop<'TSuccess, 'TFailure> и вспомогательный метод для гидратации типа

type ResultInterop<'TSuccess, 'TFailure> = {
    IsSuccess : bool
    Success : 'TSuccess
    Failure : 'TFailure
}

let toResultInterop result =
    match result with
    | Success s -> { IsSuccess=true; Success=s; Failure=Unchecked.defaultof<_> }
    | Failure f -> { IsSuccess=false; Success=Unchecked.defaultof<_>; Failure=f }

Теперь у меня есть выбор трубопровода через toResultInterop на границе F # или делать это внутри кода С#.

На границе F #

module MyFSharpModule =
    let validate request = 
        if request.isValid then
            Success "Woot"
        else
            Failure "request not valid"

    let handleUpdateRequest request = 
        request
        |> validate
        |> toResultInterop

public string Get(Request request)
{
    var result = MyFSharpModule.handleUpdateRequest(request);
    if (result.IsSuccess)
        return result.Success;
    else
        throw new Exception(result.Failure);
}

После взаимодействия в Csharp

module MyFSharpModule =
    let validate request = 
        if request.isValid then
            Success "Woot"
        else
            Failure "request not valid"

    let handleUpdateRequest request = request |> validate

public string Get(Request request)
{
    var response = MyFSharpModule.handleUpdateRequest(request);
    var result = Interop.toResultInterop(response);
    if (result.IsSuccess)
        return result.Success;
    else
        throw new Exception(result.Failure);
}

Ответ 6

Как насчет этого? Это вдохновлено @Mauricio Scheffer комментарием выше и кодом CSharpCompat в FSharpx.

С#:

MyUnion u = CallIntoFSharpCode();
string s = u.Match(
  ifFoo: () => "Foo!",
  ifBar: (b) => $"Bar {b}!");

F #:

  type MyUnion =
    | Foo
    | Bar of int
  with
    member x.Match (ifFoo: System.Func<_>, ifBar: System.Func<_,_>) =
      match x with
      | Foo -> ifFoo.Invoke()
      | Bar b -> ifBar.Invoke(b)

Что мне больше всего нравится в этом, так это то, что он исключает возможность ошибки во время выполнения. У вас больше нет поддельного по умолчанию кода для кодирования, и при изменении типа F # (например, добавление регистра) код С# не будет компилироваться.

Ответ 7

Я использую следующие методы для взаимодействия союзов из библиотеки F # на хост С#. Это может добавить некоторое время выполнения из-за использования отражения и должно быть проверено, вероятно, модульными тестами, для обработки правильных универсальных типов для каждого случая объединения.

  1. На стороне F #
type Command = 
     | First of FirstCommand
     | Second of SecondCommand * int

module Extentions =
    let private getFromUnionObj value =
        match value.GetType() with 
        | x when FSharpType.IsUnion x -> 
            let (_, objects) = FSharpValue.GetUnionFields(value, x)
            objects                        
        | _ -> failwithf "Can't parse union"

    let getFromUnion<'r> value =    
        let x = value |> getFromUnionObj
        (x.[0] :?> 'r)

    let getFromUnion2<'r1,'r2> value =    
        let x = value |> getFromUnionObj
        (x.[0] :?> 'r1, x.[1] :? 'r2)
  1. На стороне С#
        public static void Handle(Command command)
        {
            switch (command)
            {
                case var c when c.IsFirstCommand:
                    var data = Extentions.getFromUnion<FirstCommand>(change);
                    // Handler for case
                    break;
                case var c when c.IsSecondCommand:
                    var data2 = Extentions.getFromUnion2<SecondCommand, int>(change);
                    // Handler for case
                    break;
            }
        }