Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a Python equivalent for Scala's Option or Either?

I really enjoy using the Option and Either monads in Scala. Are there any equivalent for these things in Python? If there aren't, then what is the pythonic way of handling errors or "absence of value" without throwing exceptions?

like image 368
Melvic Ybanez Avatar asked Apr 10 '14 15:04

Melvic Ybanez


People also ask

Does Python have options?

Natively, Python has a Literal Type Optional but it's not the same. Alternatively this is a representation of the Either data type for python 3.

What is Monad in Python?

A monad is a design pattern that allows us to add a context to data values, and also allows us to easily compose existing functions so that they execute in a context aware manner.


2 Answers

The pythonic way for a function to say "I am not defined at this point" is to raise an exception.

>>> int("blarg")
Traceback (most recent call last):
  ...
ValueError: invalid literal for int() with base 10: 'blarg'

>>> dict(foo=5)['bar']
Traceback (most recent call last):
  ...
KeyError: 'bar'

>>> 1 / 0
Traceback (most recent call last):
  ...
ZeroDivisionError: integer division or modulo by zero

This is, in part, because there's no (generally useful) static type checker for python. A Python function cannot syntactically state, at compile time, that it has a particular codomain; there's no way to force callers to match all of the cases in the function's return type.

If you prefer, you can write (unpythonically) a Maybe wrapper:

class Maybe(object):
    def get_or_else(self, default):
        return self.value if isinstance(self, Just) else default

class Just(Maybe):
    def __init__(self, value):
        self.value = value

class Nothing(Maybe):
    pass

But I would not do this, unless you're trying to port something from Scala to Python without changing much.

like image 68
SingleNegationElimination Avatar answered Oct 16 '22 06:10

SingleNegationElimination


You can play with typing package (Python 3.6.9). Using following makes type checker happy

from typing import Optional, Union


def parse_int(s: str) -> Optional[int]:
    try:
        return int(s)
    except:
        return None


print('-- optional --')
print(parse_int('123'))
print(parse_int('a'))


def parse_int2(s: str) -> Union[str, int]:
    try:
        return int(s)
    except Exception as e:
        return f'Error during parsing "{s}": {e}'


print('-- either --')
print(parse_int2('123'))
print(parse_int2('a'))

Result

-- optional --
123
None
-- either --
123
Error during parsing "a": invalid literal for int() with base 10: 'a'

If you want to add monadic behaviour to Either you can try this

from typing import TypeVar, Generic, Callable

A = TypeVar('A')
B = TypeVar('B')
C = TypeVar('C')

Either = NewType('Either', Union['Left[A]', 'Right[C]'])


class Left(Generic[A]):
    def __init__(self, value: A):
        self.__value = value

    def get(self) -> A:
        raise Exception('it is left')

    def get_left(self) -> A:
        return self.__value

    def flat_map(self, f: Callable[[B], Either]) -> Either:
        return self

    def map(self, f: Callable[[B], C]) -> Either:
        return self

    def __str__(self):
        return f'Left({self.__value})'

and right type

class Right(Generic[B]):
    def __init__(self, value: B):
        self.__value = value

    def flat_map(self, f: Callable[[B], Either]) -> Either:
        return f(self.__value)

    def map(self, f: Callable[[B], C]) -> Either:
        return Right(f(self.__value))

    def __str__(self):
        return f'Right({self.__value})'


def parse_int(s: str) -> Union[Left[str], Right[int]]:
    try:
        return Right(int(s))
    except Exception as e:
        return Left(f'Error during parsing {s}: {e}')

def divide(x: int) -> Union[Left[str], Right[int]]:
    return Right(4 / x) if x != 0 else Left('zero !!!')

print(parse_int('1').map(lambda x: x * 2))
print(parse_int('a').map(lambda x: x * 2))
print(parse_int('2').flat_map(divide))
print(parse_int('0').flat_map(divide))

Result

Right(2)
Left(Error during parsing a: invalid literal for int() with base 10: 'a')
Right(2.0)
Left(zero !!!)
like image 16
slavik Avatar answered Oct 16 '22 05:10

slavik