Как избежать дублирования кода в F # - PullRequest
11 голосов
/ 14 февраля 2010

У меня есть два фрагмента кода, который пытается преобразовать список с плавающей точкой в ​​список Vector3 или Vector2. Идея состоит в том, чтобы брать 2/3 элемента за раз из списка и объединять их как вектор. Конечным результатом является последовательность векторов.

    let rec vec3Seq floatList =
        seq {
            match floatList with
            | x::y::z::tail -> yield Vector3(x,y,z)
                               yield! vec3Seq tail
            | [] -> ()
            | _ -> failwith "float array not multiple of 3?"
            }

    let rec vec2Seq floatList =
        seq {
            match floatList with
            | x::y::tail -> yield Vector2(x,y)
                            yield! vec2Seq tail
            | [] -> ()
            | _ -> failwith "float array not multiple of 2?"
            }

Код выглядит очень похоже, но, похоже, нет способа извлечь общую часть. Есть идеи?

Ответы [ 4 ]

13 голосов
/ 14 февраля 2010

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

let rec mkSeq (|P|_|) x =
  seq {
    match x with
    | P(p,tail) -> 
        yield p
        yield! mkSeq (|P|_|) tail
    | [] -> ()
    | _ -> failwith "List length mismatch" }

let vec3Seq =
  mkSeq (function
  | x::y::z::tail -> Some(Vector3(x,y,z), tail)
  | _ -> None)
2 голосов
/ 14 февраля 2010

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

let rec listToSeq convert (list:list<_>) =
    seq {
        if not(List.isEmpty list) then
            let list, vec = convert list
            yield vec
            yield! listToSeq convert list
        }

let vec2Seq = listToSeq (function
    | x::y::tail -> tail, Vector2(x,y)
    | _ -> failwith "float array not multiple of 2?")

let vec3Seq = listToSeq (function
    | x::y::z::tail -> tail, Vector3(x,y,z)
    | _ -> failwith "float array not multiple of 3?")
2 голосов
/ 14 февраля 2010

Как прокомментировал Рекс, если вы хотите это только для двух случаев, то у вас, вероятно, не возникнет никаких проблем, если вы оставите код как есть. Однако, если вы хотите извлечь общий шаблон, вы можете написать функцию, которая разбивает список на подсписок заданной длины (2 или 3 или любое другое число). После этого вы будете использовать map, чтобы превратить каждый список указанной длины в Vector.

Функция разделения списка недоступна в библиотеке F # (насколько я могу судить), поэтому вам придется реализовать ее самостоятельно. Это можно сделать примерно так:

let divideList n list = 
  // 'acc' - accumulates the resulting sub-lists (reversed order)
  // 'tmp' - stores values of the current sub-list (reversed order)
  // 'c'   - the length of 'tmp' so far
  // 'list' - the remaining elements to process
  let rec divideListAux acc tmp c list = 
    match list with
    | x::xs when c = n - 1 -> 
      // we're adding last element to 'tmp', 
      // so we reverse it and add it to accumulator
      divideListAux ((List.rev (x::tmp))::acc) [] 0 xs
    | x::xs ->
      // add one more value to 'tmp'
      divideListAux acc (x::tmp) (c+1) xs
    | [] when c = 0 ->  List.rev acc // no more elements and empty 'tmp'
    | _ -> failwithf "not multiple of %d" n // non-empty 'tmp'
  divideListAux [] [] 0 list      

Теперь вы можете использовать эту функцию для реализации двух ваших преобразований следующим образом:

seq { for [x; y] in floatList |> divideList 2 -> Vector2(x,y) }
seq { for [x; y; z] in floatList |> divideList 3 -> Vector3(x,y,z) }

Это выдаст предупреждение, потому что мы используем неполный шаблон, который ожидает, что возвращаемые списки будут иметь длину 2 или 3 соответственно, но это правильное ожидание, поэтому код будет работать нормально. Я также использую краткую версию выражения последовательности -> делает то же самое, что и do yield, но его можно использовать только в простых случаях, подобных этому.

0 голосов
/ 14 февраля 2010

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

// take 3 [1 .. 5] returns ([1; 2; 3], [4; 5])
let rec take count l =
    match count, l with
    | 0, xs -> [], xs
    | n, x::xs -> let res, xs' = take (count - 1) xs in x::res, xs'
    | n, [] -> failwith "Index out of range"

// split 3 [1 .. 6] returns [[1;2;3]; [4;5;6]]
let rec split count l =
    seq { match take count l with
          | xs, ys -> yield xs; if ys <> [] then yield! split count ys }

let vec3Seq l = split 3 l |> Seq.map (fun [x;y;z] -> Vector3(x, y, z))
let vec2Seq l = split 2 l |> Seq.map (fun [x;y] -> Vector2(x, y))

Теперь процесс разбиения ваших списков перенесен в его собственные общие функции "take" и "split", теперь намного проще отобразить его на нужный тип.

...