Skip to main content

Python implementation of Scala-like monadic data types.

Project description

Functional Python - Scala-like monadic data types

Functional Python is a framework which implements Scala-like monadic data types, such as Option or Map.

Also implements final class decoration and AnyVal.

Why?

Method chaining
# ToDo: Example
Type Safety
# ToDo: Example

Api Description

Options

Represents optional values. Instances of Option are either an instance of Some or the object Option.empty. Options are generics of single type parameter.

Creating an Option
from functional.option import *

# Scala-like constructor
x = Some(4)      # Some(4)
y = Option.empty # Option.empty
z = none         # Option.empty

# Python-like constructor
x = Option(4)    # Some(4)
y = Option(None) # Option.empty

Note that None which is printed is not Python None but is special object which does not contain any value and equals to Option(None):

from functional.option import *

print(str(Option.empty))     # "None"
print(repr(Option.empty))    # "Option.empty"
print(Option.empty is none)  # True
print(Option.empty is None)  # False
Getting value of an Option

Options implement .get property and .getOrElse(default) method. First one checks Option is not empty and either returns value or throws an exception. Second one returns default instead of throwing an exception.

from functional.option import *
x = Some(4)      # Some(4)
y = none         # None

x.get            # 4
y.get            # raises EmptyOption

x.get_or_else(5) # 4
y.get_or_else(5) # 5

# .is_defined returns True if Option is not None
x.is_defined     # True
y.is_defined     # False

# .is_empty is the opposite
x.is_empty       # False
y.is_empty       # True

# .non_empty is the same as .is_defined
x.non_empty      # True
y.non_empty      # False

Note that unlike in Scala, this Option's .get_or_else is not lazy-evaluated, so this code will fail:

Some(4).get_or_else(1/0)

To prevent, it is recommended use python-like accessors (see below).

Mapping an Option

Options are both functors and monads, meaning they possess .map() and .flat_map() methods with the following signatures (where object is a type Option[A]):

  • .map(f: A => B): Option[B] - map value inside an Option.
  • .flat_map(f: A => Option[B]): Option[B] - map value inside an Option to an Option.

Both these methods work only on non-empty options, returning Option.empty for otherwise.

from functional.option import *
x = Some(4)            # Some(4)
y = none               # None
z = Some(6)            # Some(6)

x.map(lambda v: v + 2) # Some(6)
y.map(lambda v: v + 2) # None
z.map(lambda v: v + 2) # Some(8)

x.flat_map(lambda v: Some(v) if v < 5 else none) # Some(4)
y.flat_map(lambda v: Some(v) if v < 5 else none) # None
z.flat_map(lambda v: Some(v) if v < 5 else none) # None
Flattening an Option

Sometimes you get an Option which contains Option. There is special property .flatten which converts Option[Option[T]] into Option[T]

# ToDo: Example
Python-style accessors

Options support python-like accessors / converters __bool__, __iter__, __len__, and __enter__/__exit.

# ToDo: Example

Final Classes

Final classes are guarded from being inherited.

from functional.final import final_class

@final_class
class MyFinalClass:
    def __init__(self, x):
        self.x = x

# The following would raise FinalInheritanceError
class ChildClass(MyFinalClass):
    def __init__(self, x = 5):
        super().__init__(x)

This is implemented by changing their __init_subclass__ method with the one throwing error. However, any parent __init_sublass__ are safe:

from functional.final import final_class

class A:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.x = 4

@final_class
class B(A):
    pass

print(B.x) # Prints 4

AnyVal

AnyVal is a helper abstract class to make Scala-like AnyVal's. It is a dataclass-like class with the only field value, constructor, hash, representation, and equals, as well as encode/decode methods.

AnyVal subclasses are made to be final. Field value is supposed to be write-protected.

Generally, works similar to typing.NewType, but the field value MUST be accepted explicitly.

from functional.anyval import AnyVal
class CustomID(AnyVal[str]): pass
class OtherAnyVal(AnyVal[str]): pass

custom_id = CustomID('1tt3s')
print(custom_id == '1tt3s')              # False
print(custom_id.value == '1tt3s')        # True
print(custom_id == OtherAnyVal('1tt3s')) # False

If package dataclasses-json is installed, AnyVal subclasses are registered to have simple decoders and encoders. If the data type could not be handled by JSON or DataClassesJSON library, you can override methods decode_value and encode_value

from datetime import date
from dataclasses_json import DataClassJsonMixin
from dataclasses import dataclass

from functional.anyval import AnyVal

class MyID(AnyVal[int]): pass
class Date(AnyVal[date]):
    @classmethod
    def decode_value(cls, data: str) -> date:
        if (not isinstance(data, str)):
            raise TypeError(f"Cannot decode {date.__name__!r} from type {type(data).__qualname__!r}, ISO-format string required")
        
        return date.fromisoformat(data)
    
    def encode_value(self) -> str:
        return self.value.isoformat()

@dataclass
class Person(DataClassJsonMixin):
    id: MyID
    name: str
    born: Date

peter = Person(MyID(15), name='peter', born=Date(date(1995, 7, 25)))
mark = Person(MyID(-131239231231), name='mark', born=Date(date.fromisoformat('2002-06-15')))

print(peter.to_json()) # {"id": 15, "name": "peter", "born": "1990-01-12"}
print(mark.to_json())  # {"id": -131239231231, "name": "mark", "born": "2002-06-15"}

print(Person.from_json('''{ "name": "Dave", "born": "2021-07-05", "id": 61123236 }'''))
# Person(id=MyID(61123236), name='Dave', born=datetime.date(2021, 7, 5))

Filters

This package provides classes for filtering. Filters are function-like classes those can, well, filter the given sequence for the given conditions.

The main difference between them and normal functions or lambdas is the fact all classes in this file are frozen dataclasses and support hashing.

List of implemented filters:

  • IsNotNoneFilter
  • HasAttrFilter
  • AndFilter
  • OrFilter
  • NotFilter

Usage

When inheriting from this class, make sure:

  • check_element method is implemented
  • Class is marked with dataclass(frozen=True) decoration
from dataclasses import dataclass
from functional.filters import AbstractFilter

@dataclass(frozen=True)
class GEFilter(AbstractFilter[int]):
    than: int
    
    def check_element(self, el: int) -> bool:
        return el >= self.than

lst = [ -1, 3, 8, 5, 0, -6, 7 ]
for el in GEFilter(5).filter(lst):
    print(el)

# Output:
# 8 5 7

Other sub-packages:

Chain Tools

functional.chaintools provides a number of functions those support method chaining.

  • functional.chaintools.chunks(): Splits an iterable into chunks of given size.
  • functional.chaintools.apply(): Synchronously apply the given function func to all elements of iterable coll. Logically same as list(map(func, coll)), but looks prettier.
  • functional.chaintools.apply_items(): Synchronously apply the given function func to all elements of iterable of iterables coll. Function results are ignored. Logically same as list(map(list, map_items(func, coll))), but looks prettier.
  • functional.chaintools.map_items(): Lazy apply function func to all elements of the given iterable of iterables coll.
  • functional.chaintools.chain(): Chain methods mapping for the given element. Just looks prettier than func5(func4(func3(func2(func1(el))))).
  • functional.chaintools.chain_map(): Chain methods mapping for the given iterable. Just looks prettier than map(lambda el: func5(func4(func3(func2(func1(el))))), coll) or even evil devil map(func5, map(func4, map(func3, map(func2, map(func1, el))))).
  • functional.chaintools.chain_map_items(): Chain methods mapping for the given iterable. Just looks prettier than gen = ((func5(func4(func3(func2(func1(el))))) for el in it) for it in coll)
  • functional.chaintools.invcall(): Version of functional.predef.call() with inversed arguments order.
  • functional.chaintools.invmap(): Version of builtins.map() with inversed arguments order.
  • functional.chaintools.invmap_items(): Version of functional.chaintools.map_items() with inversed arguments order.

Utilities:

functional.util -- miscellaneous functions and classes used across the package.

  • functional.util.unmake_dataclass(): Unregisters the given class cls from being a dataclass. It keeps all its DataClass properties (including dataclass nesting potential) but not its __dataclass_fields__ which are the only criteria dataclasses.is_dataclass() decides if the argument is a dataclass.
  • functional.util.PrettyException: Abstract class providing base to all exceptions in the package.

Map

TODO

Plans

  • Test coverage
  • Support Maps (both mutable and immutable)
  • Support Lists (both mutable and immutable)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

functional-python-0.3.1.tar.gz (20.2 kB view hashes)

Uploaded source

Built Distribution

functional_python-0.3.1-py3-none-any.whl (21.9 kB view hashes)

Uploaded py3

Supported by

AWS AWS Cloud computing Datadog Datadog Monitoring Facebook / Instagram Facebook / Instagram PSF Sponsor Fastly Fastly CDN Google Google Object Storage and Download Analytics Huawei Huawei PSF Sponsor Microsoft Microsoft PSF Sponsor NVIDIA NVIDIA PSF Sponsor Pingdom Pingdom Monitoring Salesforce Salesforce PSF Sponsor Sentry Sentry Error logging StatusPage StatusPage Status page