Определить значение по умолчанию для различаемого объединения - PullRequest
0 голосов
/ 12 мая 2018

Я хотел бы определить значение по умолчанию для различаемого объединения, например:

open System.Linq

type Result =
| Ok
| Error

let results : seq<Result> = [] |> Seq.ofList

let firstResult = results.FirstOrDefault()
// I want firstResult to be Error, currently it is null.

option <'a> работает таким образом (firstResult будет None), поэтому это должно быть возможно. Спасибо за вашу помощь.

Edit: Я использую SQLDataProvider и хотел бы написать код вроде

let userEmail =
    query {
        for user in dbContext.Public.Users do
        where (user.Id = 42)
        select (Ok user.Email)
        headOrDefault
        // should result in Error
        // when no user with Id=42 exists 
    }

Мой фактический тип результата выглядит так:

type Result<'a> =
| Ok of 'a
| Failure of string // Expected, e. g. trying to log in with a wrong password
| Error // Unexpected

Возвращая опцию, вызывающая сторона не сможет различить сбои и ошибки.

Ответы [ 3 ]

0 голосов
/ 12 мая 2018

Вы можете сделать это, используя параметр компиляции UseNullAsTrueValue.Это говорит компилятору использовать null как внутреннее представление одного из случаев без параметров.Это что-то вроде хака (потому что это в основном предназначено для оптимизации производительности, и здесь мы несколько злоупотребляем этим, но это может сработать).

У меня нет установки базы данных SQL, чтобы попробовать это с SQLпровайдер, но в памяти работает следующее, и я думаю, что оно должно работать и с SQL:

[<CompilationRepresentation(CompilationRepresentationFlags.UseNullAsTrueValue)>]
type Result<'a> =
  | Ok of 'a
  | Failure of string
  | Error

let userEmail =
  query {
    for a in [1] do
    where (a = 2)
    select (Ok a)
    headOrDefault }

Если вы запустите это, F # будет печатать интерактивно userEmail = null, но это нормально, и верно следующее:

userEmail = Error
0 голосов
/ 12 мая 2018

Если вы хотите представить проблемы с данными или параметрами запроса, например, не найти конкретную запись, отличную от других типов сбоев, например, не подключиться к базе данных или выполнить необработанное исключение, вы можете создать дискриминационный союз для явного представления ваших ожидаемых проблем с данными / запросами. Затем вы можете вернуть этот DU вместо string в качестве данных для случая Error, и вам действительно не понадобится случай Failure. Рассмотрим что-то вроде этого:

type SqlError =
| NoMatchingRecordsFound of SqlParameter list
| CouldNotConnectToDatabase
| UserDoesNotHaveQueryPermissions
| UnhandledException of exn

Я бы посоветовал взглянуть на Железнодорожно-ориентированное программирование и следовать шаблону определения дискриминированного объединения для различных случаев ошибок. Вы все еще можете включить сообщение об ошибке в этот DU, но я бы предложил использовать явные случаи для всех ваших различных ожидаемых отказов, а затем иметь «UnhandledException» или аналогичный случай для ваших непредвиденных ошибок, возможно, с exn для данных.

Если вам интересно, у меня есть библиотека на GitHub / NuGet , которая объединяет все элементы ROP и добавляет совместимость с Task, Async, и Lazy печатает в одном компоновщике вычислений, поэтому вам не нужно включать весь этот код в ваш собственный проект.

EDIT

Вот полный пример выполнения этого в стиле ROP (с использованием связанной структуры):

open FSharp.Control
open System.Linq

// Simulate user table from type-provider
[<AllowNullLiteral>]
type User() =
    member val Id = 0 with get,set
    member val Name = "" with get,set

// Simulate a SQL DB
let users = [User(Id = 42, Name = "The User")].AsQueryable()

// Our possible events from the SQL query
type SqlEvent =
| FoundUser
| UserIdDoesNotExist of int
| CouldNotConnectToDatabase
| UnhandledException of exn

// Railway-Oriented function to find the user by id or return the correct error event(s)
let getUserById id =
    operation {
        let user =
            query {
                for user in users do
                where (user.Id = id)
                select user
                headOrDefault
            }

        return!
            if user |> isNull
            then Result.failure [UserIdDoesNotExist id]
            else Result.successWithEvents user [FoundUser]
    }

Вызов getUserById 42 вернет успешно завершенную операцию с пользователем и событие FoundUser. Вызов getUserById для любого другого номера (например, 0) вернет неудачную операцию с событием ошибки UserIdDoesNotExist 0. При необходимости вы добавите больше событий.

0 голосов
/ 12 мая 2018

В общем, F # избегает концепции значений по умолчанию, вместо этого делая все как можно более явным.Более идиоматично возвращать опцию, а вызывающему абоненту решать, что использовать по умолчанию для конкретного варианта использования.

Метод FirstOrDefault может возвращать значение по умолчанию .NET только для любого типа.Таким образом, для любого класса он вернул бы null, а для числа он вернул бы ноль.

Я бы рекомендовал этот подход, вместо этого предполагая, что желаемым значением по умолчанию является Ok:

results |> Seq.tryHead |> Option.defaultValue Ok
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...