Как лучше запоминать только на основе аргумента, а не закрытия функции и внутри класса?

(вопрос отредактирован и переписан с учетом результатов обсуждения в чате)

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

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

f x = state { return DateTime.Now.AddMinutes(x) }
g x = state { return DateTime.Now.AddMinutes(x) }

Здесь g 10 и f 10 должны давать один и тот же результат, они не могут отличаться в результате двойного вызова DateTime.Now, т. е. они должны быть детерминированными. В качестве аргумента, состояние переменной здесь x.

Точно так же (g 10) - (f 5) должно давать ровно 5 minutes, а не на микросекунду больше или меньше.


Узнав, что кэширование не работает, я упростил более сложное решение до минимума, используя Шаблон запоминания Дона Сайма с картами (или dict).

Шаблон запоминания:

module Cache =
    let cache f = 
        let _cache = ref Map.empty
        fun x ->
        match (!_cache).TryFind(x) with
        | Some res -> res
        | None ->
             let res = f x
             _cache := (!_cache).Add(x,res)
             res

Кэширование предполагается использовать как часть построителя вычислений в методе Run:

type someBuilder() =
    member __.Run f = 
        Log.time "Calling __.Run"
        let memo_me =
            fun state ->
                let res = 
                    match f with
                    | State expr - expr state
                    | Value v -> state, v
                Log.time ("Cache miss, adding key: %A", s)
                res

        XCache.cache memo_me

Это не работает, потому что функция кеша каждый раз разная из-за закрытия, что приводит к промаху кеша каждый раз. Он должен быть независим от expr выше и зависеть только от state.


Я попытался разместить _cache вне функции кеша на уровне модуля, но тогда возникает проблема обобщения:

Ограничение стоимости. Предполагается, что значение _cache имеет универсальный тип
Либо определите _cache как простой термин данных, сделайте его функцией с явными аргументами, либо, если вы не хотите, чтобы он был универсальным, добавьте аннотацию типа .

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

let _cache<'T, 'U when 'T: comparison> ref : Map<'T, 'U>  = ref Map.empty

Edit, рабочая версия всего построителя вычислений

Вот построитель вычислений, как было сказано в комментариях, протестировано в FSI. Кэширование должно зависеть исключительно от TState, а не от 'TState -> 'TState * 'TResult в целом.

type State<'TState, 'TResult> = State of ('TState -> 'TState * 'TResult)

type ResultState<'TState, 'TResult> =
    | Expression of State<'TState, 'TResult>
    | Value of 'TResult

type RS<'S, 'T> = ResultState<'S, 'T>

type RS =
    static member run v s =
        match v with
        | Value item -> s, item
        | Expression (State expr) -> expr s

    static member bind k v =
        match v with
        | Expression (State expr) ->
            Expression
            <|  State
               (fun initialState ->
                let updatedState, result = expr initialState
                RS.run (k result) updatedState
               )
        | Value item -> k item

type MyBuilder() =
    member __.Bind (e, f) = RS.bind f e    
    member __.Return v = RS.Value v    
    member __.ReturnFrom e = e    
    member __.Run f = 
        printfn "Running!"
        // add/remove the first following line to see it with caching
        XCache.cache <|
            fun s ->
            match f with
            | RS.Expression (State state) -> 
                printfn "Call me once!"
                state s
            | RS.Value v -> s, v

module Builders =
    let builder = new MyBuilder()

    // constructing prints "Running!", this is as expected
    let create() = builder {
            let! v = RS.Expression <| (State <| fun i -> (fst i + 12.0, snd i + 3), "my value")
            return "test " + v
        }

    // for seeing the effect, recreating the builder twice, 
    // it should be cached once
    let result1() = create()(30.0, 39)
    let result2() = create()(30.0, 39) 

Результат запуска примера в FSI:

Выполняется!
Позвоните мне один раз!
val it : (float * int) * string = ((42.0, 42), "проверить мое значение")
Позвоните мне один раз!
val it : ( float * int) * string = ((42.0, 42), "проверить мое значение")


person Abel    schedule 23.11.2015    source источник
comment
Как вы это настроили, кеш нигде не сохраняется между запусками. Вам нужно будет создать локальную переменную в классе.   -  person John Palmer    schedule 23.11.2015
comment
@JohnPalmer: кажется, что каким бы способом я ни пытался его повернуть, я либо сталкиваюсь с проблемой дженериков (ожидалось, что выражение будет иметь тип obj, но здесь есть 'a * 'b, или что-то подобное, говорящее, что (int * int) делает не поддерживает IComparable). Я прислушиваюсь к вашему совету, но я не могу найти способ заставить его работать, кажется, что дженерики убиваются, как только я пытаюсь использовать оператор let на уровне модуля для захвата запоминаемых функций. Это похоже на это, но не то же самое: stackoverflow.com/questions/11845285/   -  person Abel    schedule 23.11.2015
comment
Можете ли вы предоставить код, использующий построитель вычислений?   -  person Fyodor Soikin    schedule 23.11.2015
comment
@FyodorSoikin, я попытаюсь создать управляемый и значительный пример.   -  person Abel    schedule 23.11.2015
comment
Моя теория выполнения до сих пор заключается в том, что вы фактически делаете несколько вызовов Run вместо того, чтобы делать только один вызов, а затем повторно вызывать результат. Но давайте посмотрим на примере.   -  person Fyodor Soikin    schedule 23.11.2015
comment
@FyodorSoikin, я добавил построитель вычислений, надеюсь, этого достаточно, чтобы проиллюстрировать его работу, идеи. Я намеренно разделил run для привязки и run для стартера CU, так как я хочу кэшировать только один CU целиком, а не промежуточные результаты, которые мало что добавят.   -  person Abel    schedule 23.11.2015
comment
Почему ключевое слово ref?   -  person Kasey Speakman    schedule 23.11.2015
comment
@Abel Моя ошибка, я не дочитал до конца. :)   -  person Kasey Speakman    schedule 23.11.2015
comment
Я вижу ваш построитель вычислений, но нигде не вижу в нем Cache. Я что-то упускаю?   -  person Fyodor Soikin    schedule 23.11.2015
comment
@fyodor, первый фрагмент кода показывает, как я пытался использовать кеш.   -  person Abel    schedule 24.11.2015
comment
Но это бесполезно таким образом: вы не показываете полный пример того, что должно работать, но не работает. Предполагается, что первый пример работает, но при использовании кода может быть ошибка. Последний образец вообще не должен работать, так что здесь нет проблем.   -  person Fyodor Soikin    schedule 24.11.2015
comment
@FyodorSoikin, извините, удалил мои последние комментарии, они были сбиты с толку в ночное время.. Я обновил вопрос. Суть в том, что я хотел бы, чтобы кеш вел себя для каждого класса, а не для каждого экземпляра.   -  person Abel    schedule 24.11.2015
comment
Конечно, вы можете хранить кеш-словарь для каждого класса, а не для каждого экземпляра, но это было бы бесполезно, потому что запоминаемая функция f появляется только для каждого экземпляра. Вы не можете запомнить функцию f на уровне выше, чем вы ее освоили. Имеет смысл?   -  person Fyodor Soikin    schedule 24.11.2015
comment
@FyodorSoikin, да, это имеет смысл, и это именно суть проблемы. Поскольку результат f зависит только от state, одно и то же состояние независимо от закрытия должно давать одинаковый результат. Моя идея состояла в том (пока бесполезно), чтобы кеш зависел только от state, а не от экземпляра f для каждого экземпляра, но использование этого шаблона создает кеш для каждого экземпляра, даже если я сделаю сам _cache глобальным.   -  person Abel    schedule 24.11.2015
comment
Но state — это функция, не так ли? Вы хотите сказать, что ожидаете иметь только ограниченное количество этих функций и не позволите потребителю создавать новые?   -  person Fyodor Soikin    schedule 24.11.2015
comment
Нет, я ясно вижу, что новый state создается при каждом вызове bind. Итак, если это так, как вы ожидаете увидеть одно и то же state дважды?   -  person Fyodor Soikin    schedule 24.11.2015
comment
Давайте продолжим обсуждение в чате.   -  person Abel    schedule 24.11.2015


Ответы (1)


Просто добавьте кэш в Run

member __.Run f = 
    printfn "Running!"

    Cache.cache <|

        fun s ->
        match f with
        | RS.Expression (State state) -> 
            printfn "Call me once!"
            state s
        | RS.Value v -> s, v

и измените функцию кеша, чтобы увидеть, действительно ли он кеширует

module Cache =
    let cache f = 
        let _cache = ref Map.empty
        fun x ->
            match (!_cache).TryFind(x) with
            | Some res -> printfn "from cache";  res
            | None ->
                 let res = f x
                 _cache := (!_cache).Add(x,res)
                 printfn "to cache"
                 res

и выход

Call me once!
to cache
val it : (float * int) * string = ((42.0, 42), "test my value")

> 
from cache
val it : (float * int) * string = ((42.0, 42), "test my value")
person Functional_S    schedule 23.11.2015
comment
Спасибо за это. Кажется, что оператор обратной трубы Cache.cache <| заботится о проблемах с типом, которые у меня были с моим первым примером, я должен был знать. Теперь я также вижу, где что-то идет не так: в более широкой картине многие компоновщики создаются с одним и тем же основным аргументом (то есть с одним и тем же замыканием), но, поскольку они создаются в разных местах, они в конечном итоге становятся разными объектами, в конечном итоге запуская Run и Delay для каждого объекта, и, конечно, у каждого свой кеш. Теперь хитрость заключается в том, чтобы кеш работал с состоянием и не зависел от объекта. Чего мне не удается достичь. - person Abel; 24.11.2015
comment
На самом деле создается только один построитель, если вы не вызываете new Builder() много раз, что вам, конечно, не нужно делать, вы можете использовать один и тот же экземпляр для всех вычислений. - person Fyodor Soikin; 24.11.2015