Выражения вычислений F#: можно ли использовать их для упрощения кода?

Недавно я начал использовать вычислительные выражения для упрощения своего кода. Пока что единственным полезным для меня является MaybeBuilder, определенный следующим образом:

type internal MaybeBuilder() =

    member this.Bind(x, f) = 
        match x with
        | None -> None
        | Some a -> f a

    member this.Return(x) = 
        Some x

    member this.ReturnFrom(x) = x

Но я хотел бы изучить другие варианты использования. Одна из возможностей заключается в ситуации, с которой я сейчас сталкиваюсь. У меня есть некоторые данные, предоставленные поставщиком, который определяет симметричную матрицу. Для экономии места дана только треугольная часть матрицы, так как другая сторона просто транспонирована. Итак, если я вижу строку в csv как

азбука, определение, 123

это означает, что значение для строки abc и определения столбца равно 123. Но я не увижу такую ​​строку, как

деф, азбука, 123

потому что эта информация уже дана из-за симметричного характера матрицы.

Я загрузил все эти данные в Map<string,Map<string,float>>, и у меня есть функция, которая получает значение для любой записи, которая выглядит так:

let myLookupFunction (m:Map<string,Map<string,float>>) s1 s2 =
    let try1 =
        match m.TryFind s1 with
        |Some subMap -> subMap.TryFind s2
        |_ -> None

    match try1 with
    |Some f -> f
    |_ ->
        let try2 =
            match m.TryFind s2 with
            |Some subMap -> subMap.TryFind s1
            |_ -> None
        match try2 with
        |Some f -> f
        |_ -> failwith (sprintf "Unable to locate a value between %s and %s" s1 s2)

Теперь, когда я знаю о вычислительных выражениях, я подозреваю, что операторы соответствия можно скрыть. Я могу немного почистить его, используя MaybeBuilder, вот так

let myFunction2  (m:Map<string,Map<string,float>>) s1 s2 =
    let maybe = new MaybeBuilder()
    let try1 = maybe{
        let! subMap = m.TryFind s1
        return! subMap.TryFind s2
    }

    match try1 with
    |Some f -> f
    |_ -> 
        let try2 = maybe{
            let! subMap = m.TryFind s2
            return! subMap.TryFind s1
        }
        match try2 with
        |Some f -> f
        |_ -> failwith (sprintf "Unable to locate a value between %s and %s" s1 s2)

При этом я перешел от 4 операторов совпадения к 2. Есть ли (не надуманный) способ еще больше очистить это с помощью вычислительных выражений?


person Chechy Levas    schedule 06.04.2018    source источник


Ответы (2)


Во-первых, создавать новый MaybeBuilder каждый раз, когда вам это нужно, довольно расточительно. Вы должны сделать это один раз, желательно прямо рядом с определением самого MaybeBuilder, а затем просто везде использовать один и тот же экземпляр. Именно так работает большинство построителей вычислений.

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

let myFunction2  (m:Map<string,Map<string,float>>) s1 s2 =
    let try' (x1, x2) = maybe{
        let! subMap = m.TryFind x1
        return! subMap.TryFind x2
    }

    match try' (s1, s2) with
    |Some f -> f
    |_ ->         
        match try' (s2, s1) with
        |Some f -> f
        |_ -> failwith (sprintf "Unable to locate a value between %s and %s" s1 s2)

В-третьих, обратите внимание на шаблон, который вы используете: попробуйте это, если нет, попробуйте то, если нет, попробуйте другое и т. д. Шаблоны можно абстрагировать как функции (в этом вся суть!), поэтому давайте сделаем это. :

let orElse m f = match m with
   | Some x -> Some x
   | None -> f()

let myFunction2  (m:Map<string,Map<string,float>>) s1 s2 =
    let try' (x1, x2) = maybe{
        let! subMap = m.TryFind x1
        return! subMap.TryFind x2
    }

    let result = 
        try' (s1, s2)
        |> orElse (fun() -> try' (s2, s1))

    match result with
    |Some f -> f
    |_ -> failwith (sprintf "Unable to locate a value between %s and %s" s1 s2)

И, наконец, я думаю, что вы делаете это неправильно. То, что вам действительно нужно, это словарь с симметричным ключом, состоящим из двух частей. Так почему бы просто не сделать это?

module MyMatrix =
    type MyKey = private MyKey of string * string
    type MyMatrix = Map<MyKey, float>

    let mkMyKey s1 s2 = if s1 < s2 then MyKey (s1, s2) else MyKey (s2, s1)


let myFunction2 (m:MyMatrix.MyMatrix) s1 s2 =
    match m.TryFind (MyMatrix.mkMyKey s1 s2) with
    | Some f -> f
    | None -> failwith (sprintf "Unable to locate a value between %s and %s" s1 s2)

Здесь MyKey — это тип, который инкапсулирует пару строк, но гарантирует, что эти строки «по порядку», то есть первая лексикографически «меньше», чем вторая. Чтобы гарантировать это, я сделал конструктор типа закрытым и вместо этого предоставил функцию mkMyKey, которая правильно создает ключ (иногда называемый "умный конструктор").

Теперь вы можете свободно использовать MyKey как для построения, так и для поиска на карте. Если вы введете (a, b, 42), вы получите и (a, b, 42), и (b, a, 42).

Несколько в сторону: общая ошибка, которую я вижу в вашем коде, — это отказ от использования абстракции. Вам не нужно обрабатывать каждую часть данных на самом низком уровне. Язык позволяет вам определять концепции более высокого уровня, а затем программировать в их терминах. Используйте эту способность.

person Fyodor Soikin    schedule 06.04.2018
comment
@Soikin: Ты умнее меня. Я удалил свой ответ. Имеет ли смысл определять MyKey как 'a*'a, а MyMatrix как Map‹MyKey, 'b›? - person ; 07.04.2018
comment
@HenrikHansen Для общего повторного использования - да, конечно. Но это не похоже на широко используемую вещь. Эмпирическое правило, которое я обычно использую, таково: сначала реализуйте конкретный случай; если я обнаружу, что делаю то же самое во второй раз, подумайте об обобщении; третий раз - однозначно обобщать. - person Fyodor Soikin; 07.04.2018

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

Есть веские причины избегать исключений в F# — они медленнее (я не знаю, насколько точно, и, вероятно, это зависит от вашего варианта использования), и предполагается, что они используются в «исключительных обстоятельствах», но у языка есть хорошая поддержка для них.

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

let myLookupFunction (m:Map<string,Map<string,float>>) s1 s2 =
  try m.[s1].[s2] with _ -> 
  try m.[s2].[s1] with _ ->
    failwith (sprintf "Unable to locate a value between %s and %s" s1 s2)

Тем не менее, я полностью согласен с Федором в том, что было бы разумно определить свою собственную структуру данных для хранения данных, а не использовать карту карт (с возможным переключением ключей).

person Tomas Petricek    schedule 06.04.2018