Skip to content

Latest commit

 

History

History
430 lines (340 loc) · 12.6 KB

README.md

File metadata and controls

430 lines (340 loc) · 12.6 KB

pyMonet

Build Status Coverage Status Documentation Status PyPI version License: MIT

High abstract python library for functional programming. Contains algebraic data structures known (or unknown) from Haskell or Scala. With MIT licence. Docs

Install:

pip install pymonet

Content:

The Either type represents values with two possibilities: B value of type Either<A, B> is either Left or Right. But not both in the same time.

Maybe type is the most common way of representing nothingness (or the null type). Maybe is effectively abstract and has two concrete subtypes: Box (also Some) and Nothing.

Boxs are data-types that store values. No restriction is placed on how they store these values, though there may be restrictions on some methods if a Box is also an instance of a sub-class of Box.

In mathematics, a semigroup is an algebraic structure consisting of a set together with an associative binary operation. A semigroup generalizes a monoid in that there might not exist an identity element. It also (originally) generalized a group (a monoid with all inverses) to a type where every element did not have to have an inverse, this the name semigroup.

Lazy are data-types that store functions. Stored function will not be called until call of bind method

Implementation of list data structures with immutable methods

Task are data-type for handle execution of functions (in lazy way) transform results of this function and handle errors.

The Try control gives us the ability write safe code without focusing on try-catch blocks in the presence of exceptions.

It that can hold either a success value or a failure value and has methods for accumulating errors.

Set of functional programming helpers

Either

The Either type represents values with two possibilities: B value of type Either<A, B> is either Left or Right. But not both in the same time. Left represents error value so any maps and bind will NOT be applied on it.

from pymonet.either import Left, Right
from pymonet.utils import identity

def divide(divided, divider):
    if divider == 0:
        return Left('can not divide by 0')
    return Right(divided, divider)

def handle_error(value):
    print ('error {}'.format(value))

def handle_success(value):
    print ('success {}'.format(value))

(divide(42, 0)
    .map(lambda value: value + 1)
    .bind(lambda value: Right(value + 1))
    .case(error=handle_error, success=handle_success))
# error 42

(divide(42, 1)
    .map(identity, lambda value: value + 1)
    .bind(lambda value: Right(value + 1))
    .case(error=handle_error, success=handle_success))
# success  44

Maybe

Maybe type is the most common way of representing nothingness (or the null type) with making the possibilities of NullPointer issues disappear. Maybe is effectively abstract and has two concrete subtypes: Some (also Box) and None (also Nothing).

from pymonet.Maybe import Maybe


def get_index(item):
    if item in [1,2,3]:
        return Maybe.just(42)
    return Maybe.nothing()

get_index(42).get_or_else(0)  # 0
get_index(1).get_or_else(0)  # 3

bind and map methods will be applied only when maybe is not empty

from pymonet.Maybe import Maybe


get_index(42)\
  .map(lambda value: value + 1)\
  .bind(lambda value: Maybe.just(value + 1))\
  .get_or_else(0)
# 0

get_index(1)\
  .map(lambda value: value + 1)\
  .bind(lambda value: Maybe.just(value + 1))\
  .get_or_else(0)
# 3

Filter method will be applied on maybe value and return it with or without value, depend on filter result:

from pymonet.Maybe import Maybe


get_index(42)\
    .filter(lambda value: value % 2 == 0)\
    .get_or_else(0)
# 0

get_index(3)\
    .filter(lambda value: value % 2 == 0)\
    .get_or_else(0)
# 0

get_index(2)\
    .filter(lambda value: value % 2 == 0)\
    .get_or_else(0)
# 2

Box

Boxs are data-types that store values. No restriction is placed on how they store these values, though there may be restrictions on some methods if a Box is also an instance of a sub-class of Box.

from pymonet.box import Box
box = Box(42)  # Box<42>
(box
    .map(lambda value: value + 1)  # Box<43>
    .map(lambda value: str(value))  # Box<"43">
    .map(lambda value: value[::-1])  # Box<"34">
    .bind(lambda value: "output = " + value))  # "output = 34"

Semigroups

In mathematics, a semigroup is an algebraic structure consisting of a set together with an associative binary operation. A semigroup generalizes a monoid in that there might not exist an identity element. It also (originally) generalized a group (a monoid with all inverses) to a type where every element did not have to have an inverse, this the name semigroup.

All

from pymonet.semigroups import All

All(True).concat(All(False))  # All<False>
All(True).concat(All(True))  # All<True>

== operator compares value of semigroups

All(True) == All(True)  # True
All(True) == All(False)  # False

Sum

from pymonet.semigroups import Sum

Sum(42).concat(Sum(1))  # Sum<43>
Sum(42).concat(Sum(1)).concat(Sum(1))  # Sum<44>
Sum(42).concat(Sum(1).concat(Sum(1)))  # Sum<44>

Sum(42).bind(lambda value: value)  # 42

First

from pymonet.semigroups import First

First('first').concat(First('Second'))  # First<"first">
First('first').bind(lambda value: value[::-1])  # "tsrif"

Map

from pymonet.semigroups import Sum, All, First, Map
ingredient1 = Map({'score': Sum(1), 'won': All(True), 'captain': First('captain america')})
ingredient2 = Map({'score': Sum(2), 'won': All(True), 'captain': First('iron man')})
ingredient1.concat(ingredient2)  # Map<{'score': Sum(3), 'won': All(True), 'captain': First('captain america')}>

Lazy

Lazy are data-types that store functions. Stored function will not be called until call of bind method

from pymonet.lazy import Lazy

def fn():
    print('fn call')
    return 42

def mapper(value):
    print('mapper side effect of ' + value)
    return value + 1

def side_effect(value):
    print('side effect of ' + value)

lazy = Lazy(fn)
mapped_lazy = lazy.map(mapper)
mapped_lazy.bind(side_effect)  
# fn call
# mapper side effect of 42
# side effect of 42

Lazy instances memoize output of constructor function

lazy = Lazy(fn)
value1 = lazy.get()
# fn call
value2 = lazy.get()
print(value1, value2)
# 42, 42

ImmutableList

Implementation of list data structures with immutable methods

lst = ImmutableList.of(1, 2, 3)

lst.map(increase) # ImmutableList.of(2, 3, 4)
lst.filter(lambda item: item % 2 == 0) # ImmutableList.of(2)
lst.find(lambda item: item % 2 == 0) # 2
lst.map(increase) # ImmutableList.of(2, 3, 4)
lst.reduce(lambda acc, curr: acc + curr, 0) # 6

Task

Task are data-type for handle execution of functions (in lazy way) transform results of this function and handle errors.

from pymonet.task import Task

def resolvable_fn(reject, resolve):
    print('resolve side effect')
    resolve(42)

def rejectable_fn(reject, resolve):
    print('reject side effect')
    reject(0)

resolvable_task = Task.of(resolvable_fn)
rejectable_task = Task.of(rejectable_fn)

map method will be applied only on resolvable tasks during calling bind method

resolvable_task.map(lambda value: value + 1)  # Task<() -> 43>
rejectable_task.map(lambda value: value + 1)  # Task<() -> 0>

bind method will be applied only on resolvable tasks. bind also will call stored function

def mapper(value):
    print('mapper side effect ' + value)
    return value + 1

resolvable_task.bind(mapper)
# resolve side effect
# mapper side effect 42

rejectable_task.bind(mapper)
# reject side effect

Try

The Try control gives us the ability write safe code without focusing on try-catch blocks in the presence of exceptions.

from pymonet.monad_try import Try

def divide(dividend, divisor):
    return dividend / divisor

def success_callback(value):
    print('success: {}'.format(value))

def fail_callback(error):
    print('error: {}'.format(value))

(Try.of(divide, 42, 2)
    .on_success(success_callback)
    .on_fail(fail_callback))
# success: 21

(Try.of(divide, 42, 0)
    .on_success(success_callback)
    .on_fail(fail_callback))
#error: division by zero

map method will be only applied mapper when exception was not thrown

(Try.of(divide, 42, 2)
    .map(lambda value: value + 1)
    .on_success(success_callback)
    .on_fail(fail_callback))
# success: 22

(Try.of(divide, 42, 0)
    .on_success(success_callback)
    .map(lambda value: value + 1)
    .on_fail(fail_callback))
#error: division by zero

get_or_else method returns value when exception was not thrown

Try.of(divide, 42, 2).get_or_else('Holy Grail') # 21
Try.of(divide, 42, 0).get_or_else('Holy Grail') # 'Holy Grail'

get method should return value with or without exception thrown

Try.of(divide, 42, 2).get()  # 21
Try.of(divide, 42, 0).get()  # ZeroDivisionError<'division by zero'>

Validation

It that can hold either a success value or a failure value and has methods for accumulating errors

from pymonet.validation import Validation


def test_validation_is_fail():
    assert Validation.fail(['fail']).is_fail()


def validate_length(value):
    if len(value) < 5:
        return Validation.fail(['value not long enough'])
    return Validation.success()


def validate_uppercase(value):
    if value[0].upper() != value[0]:
        return Validation.fail(['value not uppercase'])
    return Validation.success()


def validate_contains_special_character(value):
    if re.match(r'^[a-zA-Z0-9_]*$', value):
        return Validation.fail(['value not contains special character'])
    return Validation.success()


def validate(value):
    return (Validation.success(value)
            .ap(validate_length)
            .ap(validate_uppercase)
            .ap(validate_contains_special_character))


validate('Success$') # Validation['Success$', []]
validate('Success') # Validation['Success$', ['value not uppercase']]
validate('S$') # Validation['Success$', ['value not long enough']]
validate('s$') # Validation['Success$', ['value not long enough', 'value not uppercase']]
validate('s') # Validation['Success$', ['value not long enough', 'value not uppercase', 'value not contains special character']]

Utils

compose

Compose: performs right-to-left function composition.

from pymonet.utils import \
    increase,\
    compose,\
    curried_map as map,\
    curried_filter as filter

compose(
    list(range(10)),
    map(increase),
    filter(is_odd)
)
#[1, 3, 5, 7, 9]

pipe

Pipe: performs left-to-right function composition.

from pymonet.utils import increase, pipe

pipe(42, increase, lambda value: value * 2)
#86

cond

Returns a function which encapsulates if/else, if/else, ... logic. cond takes a list of (predicate, transformer) pairs. All of the arguments to fn are applied to each of the predicates in turn until one returns a truthy value, at which point fn returns the result of applying its arguments to the corresponding transformer.

from pymonet.utils import cond

fn = cond([
    (lambda arg: arg == 0, lambda: 'first'),
    (lambda arg: arg == 1, lambda: 'second'),
    (lambda arg: arg == 2, lambda: 'third').
])
fn(1) #  second
# lambda arg: arg == 2 will not be call

memoize

Creates a new function that, when invoked, caches the result of calling fn for a given argument set and returns the result. Subsequent calls to the memoized fn with the same argument set will not result in an additional call to fn; instead, the cached result for that set of arguments will be returned.

from pymonet.utils import memoize, eq

def fn(arg):
    print('fn flag')
    return arg + 1

memoized_fn = memoize(fn)
memoized_fn(42) # 43
# fn flag

memoized_fn(42) # 43
# print to called

memoized_fn(43) # 44
# fn flag