Явное кэширование требуется для членов списка типа в F # - PullRequest
0 голосов
/ 11 января 2019

Мой вопрос, вероятно, немного копает вопрос о том, насколько умным на самом деле является компилятор F #.

У меня есть модуль типа, который сканирует файл конфигурации и затем должен предоставить диапазон IP-адресов между начальным и конечным адресом.

type IpRange (config: string) =
    // Parse the config
    member __.StartIp = new MyIp(startIp)
    member __.EndIp = new MyIp(endIp)

Теперь я хотел добавить фактический диапазон, давая мне все IP-адреса, поэтому я добавил

    member __.Range = 
        let result = new List<MyIp>()
        let mutable ipRunner = __.StartIp
        while ipRunner <= __.EndIp do
            result.Add(new MyIp(ipRunner))
            ipRunner <- (ipRunner + 1)
        result

, который работает, но на самом деле не идиоматичен F #.

Затем я углубился в проблему и предложил следующие две альтернативы

let rec GetIpRangeRec (startIp: MyIp) (endIp: MyIp) (ipList: MyIp list) = 
    if startIp <= endIp then
        GetIpRangeRec (startIp + 1) endIp (ipList@[startIp])
    else
        ipList 

и

let GetIpRangeUnfold (startIp: MyIp) (endIp: MyIp) =
        startIp |> Seq.unfold(fun currentIp -> 
                                if (currentIp <= endIp) then 
                                    Some(currentIp, currentIp + 1) 
                                else 
                                    None)

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

Я мог бы решить эту проблему, используя Seq.cache (и предыдущее приведение к последовательности, где это необходимо), что привело бы к чему-то вроде

member __.Range =
    GetIpRangeRec startIp endIp []
    |> List.toSeq
    |> Seq.cache

но действительно ли это необходимо?

У меня такое ощущение, что я что-то пропустил, и компилятор F # действительно кеширует результат, не сообщая об этом явно.

Ответы [ 2 ]

0 голосов
/ 11 января 2019

Списки (обычно, по крайней мере, я предполагаю, что может быть какой-то странный крайний случай, о котором я не знаю) хранятся непосредственно как их значения. Таким образом, ваша рекурсивная функция определенно выдаст список MyIp с - они будут переоцениваться только в том случае, если вы сделали какую-то странную вещь, когда MyIp переоценивается каждый раз, когда к ней обращаются. Например, когда функция вернется, у вас будет полностью оцененный список MyIp с.

Однако есть одна небольшая проблема в том, что реализованная вами функция не особенно эффективна. Вместо этого я бы порекомендовал сделать это немного альтернативным способом:

let rec GetIpRangeRec (startIp: MyIp) (endIp: MyIp) (ipList: MyIp list) = 
    if startIp <= endIp then
        GetIpRangeRec (startIp + 1) endIp (startIp :: ipList)
    else
        List.rev ipList 

По сути, проблема в том, что каждый раз, когда вы используете оператор @ для добавления в конец списка, среда выполнения должна идти до конца списка, чтобы выполнить добавление. Это означает, что вы закончите итерацию по списку целую кучу раз. Вместо этого лучше просто добавить (т. Е. Добавить, но вперед), а затем перевернуть список непосредственно перед его возвратом. Это означает, что вам нужно пройти по списку только один раз, так как предварительное добавление всегда является операцией с постоянным временем (вы просто создаете новую запись списка с указателем на предыдущий фронт списка).

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

let GetIpRange startIp endIp = 
    let rec GetIpRangeRec (start: MyIp) (end: MyIp) (ipList: MyIp list) = 
        if start <= end then
            GetIpRangeRec (start + 1) end (start :: ipList)
        else
            List.rev ipList 

    GetIpRangeRec startIp endIp List.empty

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

Кроме того, имейте в виду, что хотя списки обычно подходят для последовательного доступа, они ужасны для случайного доступа. Если вам нужно выполнить случайный поиск в списке, я бы порекомендовал позвонить по номеру List.toArray, как только вы получите полный список обратно. Вероятно, не нужно беспокоиться, если вы просто будете последовательно итерировать по нему.

Я сделаю еще одно замечание: с точки зрения «полного путинского» функционального программирования ваша первая реализация может быть не полностью «функциональной», но единственная задействованная изменчивость скрыта внутри функции. То есть вы не изменяете ничего, что передается в функцию. Это прекрасно с точки зрения функциональной чистоты и может быть полезно для производительности. Помните, что F # сначала функциональный, а не ревностно-функциональный;)

РЕДАКТИРОВАТЬ: Просто подумал еще об одной вещи, которую я хотел бы добавить: я не знаю точно, как создаются ваши типы MyIp, но если вы можете построить их из чисел, возможно, стоит взглянуть на использование понимание последовательности, например seq {1 .. 100}, и затем добавление к map для создания MyIp s, например seq {1 .. 100} |> Seq.map makeIp |> Seq.toList. Это был бы самый простой способ, но сработал бы, только если вы можете просто указать простой диапазон номеров.

0 голосов
/ 11 января 2019

Seq ленив в F #, то есть есть преимущества для кэширования результатов иногда. F # List не ленивый, это неизменный единый связанный список, который не получит никакой выгоды от кэширования.

...