Source code for xotl.tools.fp.option

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------
# Copyright (c) Merchise Autrement [~º/~] and Contributors
# All rights reserved.
#
# This is free software; you can do what the LICENCE file allows you to.
#

"""Functional Programming *Option Type* definition.

In Programming, and Type Theory, an *option type*, or *maybe type*, represents
encapsulation of an optional value; e.g., it is used in functions which may or
may not return a meaningful value when they are applied.

It consists of either a constructor encapsulating the original value ``x``
(written ``Just x`` or ``Some x``) or an empty constructor (called *None* or
*Nothing*).  Outside of functional programming, these are known as *nullable
types*.

In our case *option type* will be the `Maybe`:class: class (the equivalent of
`Option` in *Scala Programming Language*), the wrapper for valid values will
be the `Just`:class: class (equivalent of `Some` in *Scala*); and the wrapper
for invalid values will be the `Wrong`:class: class.

Instead of *None* or *Nothing*, `Wrong` is used because two reasons:
(1) already existence of `None` special Python value, and (2) `Wrong`:class:
also wraps incorrect values and can have several instances (not only a *null*
value).

"""


[docs]class Maybe: """Wrapper for optional values. The Maybe type encapsulates an optional value. A value of type ``Maybe a`` either contains a value of type ``a`` (represented as ``Just a``), or it is empty (represented as ``Nothing``). Using `Maybe`` is a good way to deal with errors or exceptional cases without resorting to drastic measures such as error. In this implementation we make a variation where a ``Wrong`` object represents a missing (with special value ``Nothing``) or an improper value (including errors). See descendant classes `Just`:class: and `Wrong`:class: for more information. This implementation combines ``Maybe`` and ``Either`` Haskell data types. ``Maybe`` is a means of being explicit that you are not sure that a function will be successful when it is executed. Conventionally, the usage of ``Either`` for errors uses ``Right`` when the computation is successful, and ``Left`` for failing scenarios. In this implementation, `Just`:class` us used for equivalence with both Haskell ``Just`` and ``Right`` types; `Wrong`:class: is used with the special value ``Nothing`` and to encapsulate errors or incorrect values (Haskell ``Left``). Haskell:: data Maybe a = Nothing | Just a either :: (a -> c) -> (b -> c) -> Either a b -> c Case analysis for the Either type. If the value is Left a, apply the first function to a; if it is Right b, apply the second function to b. """ __slots__ = "inner" _singletons = [None, None, None] # False, True, None def __new__(cls, *args): default = cls is Just if len(args) == 0: arg = default elif len(args) == 1: arg = args[0] else: msg = '{}: receive too many arguments "{}"' raise TypeError(msg.format(cls.__name__, len(args))) if arg is default or arg is None and cls is Wrong: idx = 2 if arg is None else arg if cls._singletons[idx] is None: self = super().__new__(cls) self.inner = arg cls._singletons[idx] = self return cls._singletons[idx] elif cls is Maybe: return (Just if arg else Wrong)(arg) elif isinstance(arg, cls): return arg elif not isinstance(arg, Maybe): self = super().__new__(cls) self.inner = arg return self else: msg = "re-wrapping inverted value: {}({})" raise ValueError(msg.format(cls.__name__, arg)) def __init__(self, *args): pass def __nonzero__(self): return isinstance(self, Just) __bool__ = __nonzero__ def __str__(self): return "{}({!r})".format(type(self).__name__, self.inner) __repr__ = __str__ def __eq__(self, other): return ( isinstance(other, type(self)) and self.inner == other.inner or self.inner is other ) # TODO: check if `==` instead `is` def __ne__(self, other): return not (self == other)
[docs] @classmethod def compel(cls, value): """Coerce to the correspondent logical Boolean value. `Just`:class: is logically true, and `Wrong` is false. For example:: >>> Just.compel([1]) [1] >>> Just.compel([]) Just([]) >>> Wrong.compel([1]) Wrong([1]) >>> Wrong.compel([]) [] """ if cls is not Maybe: test = cls is Just dual = Wrong if test else Just if bool(value) is test: return value elif not isinstance(value, dual): return cls(value) else: msg = '''a "{}" value can't be coerced to "{}"''' vname = type(value).__name__ raise TypeError(msg.format(vname, cls.__name__)) else: raise TypeError("""don't call at Maybe base level""")
[docs] @classmethod def choose(cls, *types): """Decorator to force `Maybe` values constraining to expecting types. For example, a function that return a collection (tuple or list) if valid or False if not, if not decorated could be ambiguous for an empty collection:: >>> @Just.choose(tuple, list) ... def check_range(values, min, max): ... if isinstance(values, (tuple, list)): ... return [v for v in values if min <= v <= max] ... else: ... return False >>> check_range(range(10), 7, 17) [7, 8, 9] >>> check_range(range(10), 17, 27) Just([]) >>> check_range(set(range(10)), 7, 17) False """ pass
[docs]class Just(Maybe): """A wrapper for valid results.""" __slots__ = ()
[docs]class Wrong(Maybe): """A wrapper for invalid results.""" __slots__ = ()
[docs]def take(value): """Extract a value.""" return value.inner if isinstance(value, Maybe) else value
# ---- special singletons ---- #: A `Wrong`:class: special singleton encapsulating the `False` value. false = Wrong() #: A `Just`:class: special singleton encapsulating the `True` value. true = Just() #: A `Wrong`:class: special singleton encapsulating the `None` value. none = Wrong(None)