Module hoshmap.value.lazyival

Expand source code
#  Copyright (c) 2021. Davi Pereira dos Santos
#  This file is part of the hoshmap project.
#  Please respect the license - more about this in the section (*) below.
#
#  hoshmap is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  hoshmap is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with hoshmap.  If not, see <http://www.gnu.org/licenses/>.
#
#  (*) Removing authorship by any means, e.g. by distribution of derived
#  works or verbatim, obfuscated, compiled or rewritten versions of any
#  part of this work is illegal and it is unethical regarding the effort and
#  time spent here.
#
import operator
from functools import reduce
from itertools import chain
from typing import Iterable, Union

from hosh import Hosh

from hoshmap.serialization.parsing import f2hosh
from hoshmap.value.cacheableival import CacheableiVal


class LazyiVal(CacheableiVal):
    """
    Identified lazy value

    Threefold lazy: It is calculated only when needed, only once and it is cached.

    >>> from hoshmap.value import LazyiVal
    >>> cache = {}
    >>> from hoshmap.value.strictival import StrictiVal
    >>> deps = {"x": StrictiVal(2)}
    >>> lvx = LazyiVal(lambda x: x**2, 0, 1, deps, {}, caches=[cache])
    >>> lvx
    λ(x)
    >>> deps = {"x": lvx, "y": StrictiVal(3)}
    >>> result = {}
    >>> f = lambda x,y: [x+y, y**2]
    >>> lvy = LazyiVal(f, 0, 2, deps, result, caches=[cache])
    >>> lvz = LazyiVal(f, 1, 2, deps, result, caches=[cache])
    >>> lvx, lvy, lvz
    (λ(x), λ(x=λ(x) y), λ(x=λ(x) y))
    >>> deps = {"z": lvz}
    >>> f = lambda z: {"z":z**3, "w":z**5}
    >>> result = {}
    >>> lvz2 = LazyiVal(f, 0, 2, deps, result, caches=[cache])
    >>> lvw = LazyiVal(f, 1, 2, deps, result, caches=[cache])
    >>> lvx, lvy, lvz2, lvw
    (λ(x), λ(x=λ(x) y), λ(z=λ(x=λ(x) y)), λ(z=λ(x=λ(x) y)))
    >>> lvx.value, lvy.value, lvz2.value, lvw.value
    (4, 7, 729, 59049)
    >>> lvz2.id
    'Vi2uj31Ge.Qq5HvyosBKEteuKD.ia1T.smNcT1ue'
    >>> lvw.id
    '-.wMmh0A9lujq3.ILKbU0gbeCUjGZsgRgPjtpaI-'
    >>> lvz2.hosh * lvw.hosh == lvz.hosh * lvw.fhosh
    True
    """

    def __init__(
        self,
        f: callable,
        i: int,
        n: int,
        deps: dict,
        results: dict,
        fid: Union[str, Hosh] = None,
        caches=None,
        did=None,
        dids=None,
    ):
        # if i >= len(result):  # pragma: no cover
        #     raise Exception(f"Index {i} inconsistent with current expected result size {len(result)}.")
        super().__init__(caches, did, dids)
        self.f = f
        self.i = i
        self.n = n
        self.deps = {} if deps is None else deps
        self.results = results
        self.fhosh = f2hosh(f) if fid is None else self.handle_id(fid)
        self.hosh = reduce(operator.mul, chain(self.deps.values(), [self.fhosh]))[i:n]
        self.results[self.id] = Unevaluated

    def replace(self, **kwargs):
        dic = dict(f=self.f, i=self.i, n=self.n, deps=self.deps, results=self.results, fid=self.fhosh, caches=self.caches)
        dic.update(kwargs)
        return LazyiVal(**dic)

    @property
    def value(self):
        if not self.isevaluated:
            if (fetched := self.fetch()) is not LookupError:
                return fetched
            # if self.caches is None:
            #     raise Exception
            self.calculate()
            self.store()
        return self.results[self.id]

    # noinspection PyUnboundLocalVariable
    def fetch(self):
        if self.caches is not None:
            from hoshmap import FrozenIdict

            outdated_caches, found = [], False
            for cache in self.caches:
                if found:
                    if cache.ismirror and self.id not in cache:
                        cache[self.did] = {"_ids": self.dids}
                        cache[self.id] = val
                else:
                    if self.id in cache:
                        for outdated_cache in outdated_caches:
                            outdated_cache[self.did] = {"_ids": self.dids}
                        val = self.traverse(self.id, cache, outdated_caches)
                        # TODO: receber iVal de dentro do cache, nao value
                        # TODO: passar cache pra ele quando for CacheableiVal
                        self.results[self.id] = val
                        found = True
                    else:
                        outdated_caches.append(cache)
            if found:
                return val
        return LookupError

    def calculate(self):
        from hoshmap import idict

        argidxs = []
        kwargs = {}
        iterable_sources = {}
        for field, ival in self.deps.items():
            if isinstance(field, int):  # quando usa isso???
                argidxs.append(field)
            else:
                if len(split := field.split(":*")) == 2:
                    iterable_sources[split[1]] = iter(self.deps[field].value)
                else:
                    kwargs[field] = ival.value
        if iterable_sources:
            result = []
            while True:
                i = None
                try:
                    for i, (target, it) in enumerate(iterable_sources.items()):
                        kwargs[target] = next(it)
                    r = self.f(*(self.deps[idx] for idx in sorted(argidxs)), **kwargs)
                    if isinstance(r, idict):
                        r = r.frozen
                    result.append(r)
                except StopIteration:
                    if i not in [0, len(iterable_sources)]:
                        raise ValueError("All iterable fields (e.g., 'xs:*x') should have the same length.")
                    break
        else:
            result = self.f(*(self.deps[idx] for idx in sorted(argidxs)), **kwargs)
            if isinstance(result, idict):
                result = result.frozen
        if self.n == 1:
            result = [result]
        elif isinstance(result, dict):
            result = result.values()
        elif isinstance(result, list) and len(result) != self.n:  # pragma: no cover
            raise Exception(f"Wrong result length: {len(result)} differs from {self.n}")
        if not isinstance(result, Iterable):  # pragma: no cover
            raise Exception(f"Unsupported multi-valued result type: {type(result)}")
        for id, res in zip(self.results, result):
            self.results[id] = res

    def store(self):
        if self.caches is not None:
            from hoshmap import FrozenIdict

            for id, res in self.results.items():
                for cache in self.caches:
                    if self.did not in cache:
                        cache[self.did] = {"_ids": self.dids}
                    if isinstance(res, FrozenIdict):
                        # TODO:
                        #  if res=cacheableival: add current cache to res.'lazies*'.caches caso não tenham
                        #  [não faz sentido picklear caches, então...]
                        #  busca no cache anterior do subdict pelo resultado já pronto (e grava no corrente?)
                        #   senão:
                        #       se cache corrente contém fid, armazena field como lazy
                        #       senão, evaluate nesse field (p/ ser armazenado mais abaixo).
                        #
                        res.evaluate()  # <-- retirar depois de feito TODOs acima
                        if res.id not in cache:
                            # REMINDER: entry id differs from internal did
                            cache[id] = {"_ids": res.ids}
                        res >> [[cache]]
                    elif id not in cache:
                        cache[id] = res

    def traverse(self, id, cache, outdated_caches):
        if id not in cache:
            raise Exception(f"Id {id} not found.")
        val = cache[id]
        for outdated_cache in outdated_caches:
            outdated_cache[id] = val
        if isinstance(val, dict) and list(val.keys()) == ["_ids"]:
            from hoshmap import FrozenIdict

            ids = val["_ids"]
            data = {}
            for k, v in ids.items():
                data[k] = self.traverse(v, cache, outdated_caches)
            return FrozenIdict.fromdict(data, ids)
        return val

    def __reduce__(self):
        # TODO: pickling idicts shouldn't be necessary after cache[id]=DFiVal is implemented
        raise NotImplementedError
        dic = self.data.copy()
        ids = dic.pop("_ids").copy()
        del dic["_id"]
        return self.fromdict, (dic, ids)


class Unevaluated:
    pass


Unevaluated = Unevaluated()

Classes

class LazyiVal (f: , i: int, n: int, deps: dict, results: dict, fid: Union[str, hosh.hosh_.Hosh] = None, caches=None, did=None, dids=None)

Identified lazy value

Threefold lazy: It is calculated only when needed, only once and it is cached.

>>> from hoshmap.value import LazyiVal
>>> cache = {}
>>> from hoshmap.value.strictival import StrictiVal
>>> deps = {"x": StrictiVal(2)}
>>> lvx = LazyiVal(lambda x: x**2, 0, 1, deps, {}, caches=[cache])
>>> lvx
λ(x)
>>> deps = {"x": lvx, "y": StrictiVal(3)}
>>> result = {}
>>> f = lambda x,y: [x+y, y**2]
>>> lvy = LazyiVal(f, 0, 2, deps, result, caches=[cache])
>>> lvz = LazyiVal(f, 1, 2, deps, result, caches=[cache])
>>> lvx, lvy, lvz
(λ(x), λ(x=λ(x) y), λ(x=λ(x) y))
>>> deps = {"z": lvz}
>>> f = lambda z: {"z":z**3, "w":z**5}
>>> result = {}
>>> lvz2 = LazyiVal(f, 0, 2, deps, result, caches=[cache])
>>> lvw = LazyiVal(f, 1, 2, deps, result, caches=[cache])
>>> lvx, lvy, lvz2, lvw
(λ(x), λ(x=λ(x) y), λ(z=λ(x=λ(x) y)), λ(z=λ(x=λ(x) y)))
>>> lvx.value, lvy.value, lvz2.value, lvw.value
(4, 7, 729, 59049)
>>> lvz2.id
'Vi2uj31Ge.Qq5HvyosBKEteuKD.ia1T.smNcT1ue'
>>> lvw.id
'-.wMmh0A9lujq3.ILKbU0gbeCUjGZsgRgPjtpaI-'
>>> lvz2.hosh * lvw.hosh == lvz.hosh * lvw.fhosh
True
Expand source code
class LazyiVal(CacheableiVal):
    """
    Identified lazy value

    Threefold lazy: It is calculated only when needed, only once and it is cached.

    >>> from hoshmap.value import LazyiVal
    >>> cache = {}
    >>> from hoshmap.value.strictival import StrictiVal
    >>> deps = {"x": StrictiVal(2)}
    >>> lvx = LazyiVal(lambda x: x**2, 0, 1, deps, {}, caches=[cache])
    >>> lvx
    λ(x)
    >>> deps = {"x": lvx, "y": StrictiVal(3)}
    >>> result = {}
    >>> f = lambda x,y: [x+y, y**2]
    >>> lvy = LazyiVal(f, 0, 2, deps, result, caches=[cache])
    >>> lvz = LazyiVal(f, 1, 2, deps, result, caches=[cache])
    >>> lvx, lvy, lvz
    (λ(x), λ(x=λ(x) y), λ(x=λ(x) y))
    >>> deps = {"z": lvz}
    >>> f = lambda z: {"z":z**3, "w":z**5}
    >>> result = {}
    >>> lvz2 = LazyiVal(f, 0, 2, deps, result, caches=[cache])
    >>> lvw = LazyiVal(f, 1, 2, deps, result, caches=[cache])
    >>> lvx, lvy, lvz2, lvw
    (λ(x), λ(x=λ(x) y), λ(z=λ(x=λ(x) y)), λ(z=λ(x=λ(x) y)))
    >>> lvx.value, lvy.value, lvz2.value, lvw.value
    (4, 7, 729, 59049)
    >>> lvz2.id
    'Vi2uj31Ge.Qq5HvyosBKEteuKD.ia1T.smNcT1ue'
    >>> lvw.id
    '-.wMmh0A9lujq3.ILKbU0gbeCUjGZsgRgPjtpaI-'
    >>> lvz2.hosh * lvw.hosh == lvz.hosh * lvw.fhosh
    True
    """

    def __init__(
        self,
        f: callable,
        i: int,
        n: int,
        deps: dict,
        results: dict,
        fid: Union[str, Hosh] = None,
        caches=None,
        did=None,
        dids=None,
    ):
        # if i >= len(result):  # pragma: no cover
        #     raise Exception(f"Index {i} inconsistent with current expected result size {len(result)}.")
        super().__init__(caches, did, dids)
        self.f = f
        self.i = i
        self.n = n
        self.deps = {} if deps is None else deps
        self.results = results
        self.fhosh = f2hosh(f) if fid is None else self.handle_id(fid)
        self.hosh = reduce(operator.mul, chain(self.deps.values(), [self.fhosh]))[i:n]
        self.results[self.id] = Unevaluated

    def replace(self, **kwargs):
        dic = dict(f=self.f, i=self.i, n=self.n, deps=self.deps, results=self.results, fid=self.fhosh, caches=self.caches)
        dic.update(kwargs)
        return LazyiVal(**dic)

    @property
    def value(self):
        if not self.isevaluated:
            if (fetched := self.fetch()) is not LookupError:
                return fetched
            # if self.caches is None:
            #     raise Exception
            self.calculate()
            self.store()
        return self.results[self.id]

    # noinspection PyUnboundLocalVariable
    def fetch(self):
        if self.caches is not None:
            from hoshmap import FrozenIdict

            outdated_caches, found = [], False
            for cache in self.caches:
                if found:
                    if cache.ismirror and self.id not in cache:
                        cache[self.did] = {"_ids": self.dids}
                        cache[self.id] = val
                else:
                    if self.id in cache:
                        for outdated_cache in outdated_caches:
                            outdated_cache[self.did] = {"_ids": self.dids}
                        val = self.traverse(self.id, cache, outdated_caches)
                        # TODO: receber iVal de dentro do cache, nao value
                        # TODO: passar cache pra ele quando for CacheableiVal
                        self.results[self.id] = val
                        found = True
                    else:
                        outdated_caches.append(cache)
            if found:
                return val
        return LookupError

    def calculate(self):
        from hoshmap import idict

        argidxs = []
        kwargs = {}
        iterable_sources = {}
        for field, ival in self.deps.items():
            if isinstance(field, int):  # quando usa isso???
                argidxs.append(field)
            else:
                if len(split := field.split(":*")) == 2:
                    iterable_sources[split[1]] = iter(self.deps[field].value)
                else:
                    kwargs[field] = ival.value
        if iterable_sources:
            result = []
            while True:
                i = None
                try:
                    for i, (target, it) in enumerate(iterable_sources.items()):
                        kwargs[target] = next(it)
                    r = self.f(*(self.deps[idx] for idx in sorted(argidxs)), **kwargs)
                    if isinstance(r, idict):
                        r = r.frozen
                    result.append(r)
                except StopIteration:
                    if i not in [0, len(iterable_sources)]:
                        raise ValueError("All iterable fields (e.g., 'xs:*x') should have the same length.")
                    break
        else:
            result = self.f(*(self.deps[idx] for idx in sorted(argidxs)), **kwargs)
            if isinstance(result, idict):
                result = result.frozen
        if self.n == 1:
            result = [result]
        elif isinstance(result, dict):
            result = result.values()
        elif isinstance(result, list) and len(result) != self.n:  # pragma: no cover
            raise Exception(f"Wrong result length: {len(result)} differs from {self.n}")
        if not isinstance(result, Iterable):  # pragma: no cover
            raise Exception(f"Unsupported multi-valued result type: {type(result)}")
        for id, res in zip(self.results, result):
            self.results[id] = res

    def store(self):
        if self.caches is not None:
            from hoshmap import FrozenIdict

            for id, res in self.results.items():
                for cache in self.caches:
                    if self.did not in cache:
                        cache[self.did] = {"_ids": self.dids}
                    if isinstance(res, FrozenIdict):
                        # TODO:
                        #  if res=cacheableival: add current cache to res.'lazies*'.caches caso não tenham
                        #  [não faz sentido picklear caches, então...]
                        #  busca no cache anterior do subdict pelo resultado já pronto (e grava no corrente?)
                        #   senão:
                        #       se cache corrente contém fid, armazena field como lazy
                        #       senão, evaluate nesse field (p/ ser armazenado mais abaixo).
                        #
                        res.evaluate()  # <-- retirar depois de feito TODOs acima
                        if res.id not in cache:
                            # REMINDER: entry id differs from internal did
                            cache[id] = {"_ids": res.ids}
                        res >> [[cache]]
                    elif id not in cache:
                        cache[id] = res

    def traverse(self, id, cache, outdated_caches):
        if id not in cache:
            raise Exception(f"Id {id} not found.")
        val = cache[id]
        for outdated_cache in outdated_caches:
            outdated_cache[id] = val
        if isinstance(val, dict) and list(val.keys()) == ["_ids"]:
            from hoshmap import FrozenIdict

            ids = val["_ids"]
            data = {}
            for k, v in ids.items():
                data[k] = self.traverse(v, cache, outdated_caches)
            return FrozenIdict.fromdict(data, ids)
        return val

    def __reduce__(self):
        # TODO: pickling idicts shouldn't be necessary after cache[id]=DFiVal is implemented
        raise NotImplementedError
        dic = self.data.copy()
        ids = dic.pop("_ids").copy()
        del dic["_id"]
        return self.fromdict, (dic, ids)

Ancestors

Instance variables

var value : Any
Expand source code
@property
def value(self):
    if not self.isevaluated:
        if (fetched := self.fetch()) is not LookupError:
            return fetched
        # if self.caches is None:
        #     raise Exception
        self.calculate()
        self.store()
    return self.results[self.id]

Methods

def calculate(self)
Expand source code
def calculate(self):
    from hoshmap import idict

    argidxs = []
    kwargs = {}
    iterable_sources = {}
    for field, ival in self.deps.items():
        if isinstance(field, int):  # quando usa isso???
            argidxs.append(field)
        else:
            if len(split := field.split(":*")) == 2:
                iterable_sources[split[1]] = iter(self.deps[field].value)
            else:
                kwargs[field] = ival.value
    if iterable_sources:
        result = []
        while True:
            i = None
            try:
                for i, (target, it) in enumerate(iterable_sources.items()):
                    kwargs[target] = next(it)
                r = self.f(*(self.deps[idx] for idx in sorted(argidxs)), **kwargs)
                if isinstance(r, idict):
                    r = r.frozen
                result.append(r)
            except StopIteration:
                if i not in [0, len(iterable_sources)]:
                    raise ValueError("All iterable fields (e.g., 'xs:*x') should have the same length.")
                break
    else:
        result = self.f(*(self.deps[idx] for idx in sorted(argidxs)), **kwargs)
        if isinstance(result, idict):
            result = result.frozen
    if self.n == 1:
        result = [result]
    elif isinstance(result, dict):
        result = result.values()
    elif isinstance(result, list) and len(result) != self.n:  # pragma: no cover
        raise Exception(f"Wrong result length: {len(result)} differs from {self.n}")
    if not isinstance(result, Iterable):  # pragma: no cover
        raise Exception(f"Unsupported multi-valued result type: {type(result)}")
    for id, res in zip(self.results, result):
        self.results[id] = res
def fetch(self)
Expand source code
def fetch(self):
    if self.caches is not None:
        from hoshmap import FrozenIdict

        outdated_caches, found = [], False
        for cache in self.caches:
            if found:
                if cache.ismirror and self.id not in cache:
                    cache[self.did] = {"_ids": self.dids}
                    cache[self.id] = val
            else:
                if self.id in cache:
                    for outdated_cache in outdated_caches:
                        outdated_cache[self.did] = {"_ids": self.dids}
                    val = self.traverse(self.id, cache, outdated_caches)
                    # TODO: receber iVal de dentro do cache, nao value
                    # TODO: passar cache pra ele quando for CacheableiVal
                    self.results[self.id] = val
                    found = True
                else:
                    outdated_caches.append(cache)
        if found:
            return val
    return LookupError
def replace(self, **kwargs) ‑> 
Expand source code
def replace(self, **kwargs):
    dic = dict(f=self.f, i=self.i, n=self.n, deps=self.deps, results=self.results, fid=self.fhosh, caches=self.caches)
    dic.update(kwargs)
    return LazyiVal(**dic)
def store(self)
Expand source code
def store(self):
    if self.caches is not None:
        from hoshmap import FrozenIdict

        for id, res in self.results.items():
            for cache in self.caches:
                if self.did not in cache:
                    cache[self.did] = {"_ids": self.dids}
                if isinstance(res, FrozenIdict):
                    # TODO:
                    #  if res=cacheableival: add current cache to res.'lazies*'.caches caso não tenham
                    #  [não faz sentido picklear caches, então...]
                    #  busca no cache anterior do subdict pelo resultado já pronto (e grava no corrente?)
                    #   senão:
                    #       se cache corrente contém fid, armazena field como lazy
                    #       senão, evaluate nesse field (p/ ser armazenado mais abaixo).
                    #
                    res.evaluate()  # <-- retirar depois de feito TODOs acima
                    if res.id not in cache:
                        # REMINDER: entry id differs from internal did
                        cache[id] = {"_ids": res.ids}
                    res >> [[cache]]
                elif id not in cache:
                    cache[id] = res
def traverse(self, id, cache, outdated_caches)
Expand source code
def traverse(self, id, cache, outdated_caches):
    if id not in cache:
        raise Exception(f"Id {id} not found.")
    val = cache[id]
    for outdated_cache in outdated_caches:
        outdated_cache[id] = val
    if isinstance(val, dict) and list(val.keys()) == ["_ids"]:
        from hoshmap import FrozenIdict

        ids = val["_ids"]
        data = {}
        for k, v in ids.items():
            data[k] = self.traverse(v, cache, outdated_caches)
        return FrozenIdict.fromdict(data, ids)
    return val

Inherited members