Skip to main content

Functional programming in Python using Monadic types

Project description

pre-commit Lifecycle:Experimental Docs:Latest Coverage

monadic

Functional programming in Python using Monadic types.

Installation

Monadic is available on PyPI. To install, run:

pip install monadic

Alternatively, to install the latest development version, run:

pip install git+https://github.com/austinrwarner/monadic.git@develop

Introduction

Monadic is a Python library that provides a set of Monadic types and functions for functional programming in Python. The library is inspired by the functional programming primitives available in Rust, as well as pure functional programming languages such as Haskell, F#, and Elm.

The library exposes a generic Monad type that can be used to create custom Monadic types. The library also provides a set of Monadic types that are commonly used in functional programming, including:

  • Maybe types that represent values that may or may not exist.
    • Option: Represents a value that may or may not exist.
      • Some: Represents a value that exists.
      • Nothing: Represents a value that does not exist.
    • Result: Represents the result of a computation that may fail.
      • Ok: Represents a successful computation.
      • Error: Represents a failed computation.
  • Iterable types that represent collections of values.
    • List: Represents a list of values.
    • Set: Represents a set of values.
    • Dict: Represents a dictionary of key-value pairs.

What is a Monad?

Though "Monad" has a somewhat technical definition based in category theory, in practice you can think of a Monad as a type that represents a computation that can be chained together with other computations.

For example, a common practice in Python is to represent an optional value as None. However, this can lead to code that is difficult to read and maintain due to the need to check for None values. For example, consider the following code:

from typing import Optional
from dataclasses import dataclass

@dataclass
class User:
    name: str

def get_user_name(user: Optional[User]) -> Optional[str]:
    if user is None:
        return None
    else:
        return user.name

get_user_name(None) # None
get_user_name(User("John Doe")) # "John Doe"

This code is difficult to read and maintain because it requires the reader to check for None values. This can be improved by using the Option type from Monadic:

from monadic import Option, Nothing, Some
from dataclasses import dataclass

@dataclass
class User:
    name: str


def get_user_name(user: Option[User]) -> Option[str]:
    return user.map(lambda u: u.name)

get_user_name(Nothing()) # Nothing()
get_user_name(Some(User("John Doe"))) # Some("John Doe")

This code is easier to read and maintain because expresses the "happy path" of the computation, and the Option type handles the "unhappy path" of the computation. This is possible because the Option type is a Monad, and therefore supports the map method. The map method allows you to chain computations together in the context of the specific monad. In the case of the Option type, the map method will only execute the computation if the value exists. If the value does not exist, the map method will return Nothing().

In addition to the map method, the Option type also supports the bind method. The bind method is similar to the map method, but it allows you to chain computations together that return a monadic type. For example, consider the following code:

from monadic import Option, Nothing, Some
from dataclasses import dataclass

@dataclass
class User:
    name: str
    email: Option[str] = Nothing()

def get_user_email(user: User) -> Option[str]:
    return user.email

Some(User("John Doe")).bind(get_user_email) # Nothing()
Some(User("John Doe", Some("john.doe@xyz.com"))).bind(get_user_email) # Some("john.doe@xyz.com")
Nothing().bind(get_user_email) # Nothing()

In this example, we write a function that takes a User, and returns the email field of the User. The first two examples work as expcted, they are just returning the email field of the User wrapped in a Some. However, in the third example, we call that function on a Nothing. In this case, the bind method will return Nothing. There are two reasons why we might not be able to get the email field of a User. The first is that the User does not exist, and the second is that the User does not have an email field. The bind method allows us to handle both of these cases without having to check for None values.

While every monad supports the map and bind methods, some monads support additional methods. For example, the Option type also provides the default and unwrap methods. The default method allows you to specify a default value to use if the value does not exist. The unwrap method allows you to unwrap the value from the monad, but will raise an exception if the value does not exist. For example:

from monadic import Nothing, Some

Some("Hello World").default("Goodbye World") # Some("Hello World")
Nothing().default("Goodbye World") # Some("Goodbye World")

Some("Hello World").unwrap() # "Hello World"
Nothing().unwrap() # Raises an exception

default and unwrap are often used in immediate succession. For example:

from monadic import Nothing, Some

Some("Hello World").default("Goodbye World").unwrap() # "Hello World"
Nothing().default("Goodbye World").unwrap() # "Goodbye World"

Other Monadic Types

Result

The Result type is similar to the Option type, but it is used to represent the result of a computation that may fail. The Result type has two possible values: Ok and Error. The Ok value represents a successful computation, and the Error value represents a failed computation. For example:

from monadic import Result, Ok, Error

Ok("Hello World") # Ok("Hello World")
Error(TypeError()) # Error(TypeError())

The Result type supports all the same methods as the Option type, but the semantics are slightly different. If any of the chained computations raise an exception, the Result type will return an Error value. For example:

from monadic import Ok

Ok(1).map(lambda x: x / 2) # Ok(0.5)
Ok(1).map(lambda x: x / 0) # Error(ZeroDivisionError())

The Result type also has a static method, attempt, that allows you to execute a computation that may raise an exception. For example:

from monadic import Result

Result.attempt(lambda x, y: x / y, ZeroDivisionError, 1, 2) # Ok(0.5)
Result.attempt(lambda x, y: x / y, ZeroDivisionError, 1, 0) # Error(ZeroDivisionError())
Result.attempt(lambda x, y: x / y, TypeError, 1, 0) # Raises ZeroDivisionError

This is the monadic equivalent of the try/except statement in Python. It even allows you to specify the type(s) of exception to catch, and will raise an exception if the wrong type of exception is raised.

List

The List type is used to represent a list of values. The map method on the List type will apply the given function to each value in the list. For example:

from monadic import List

List([1, 2, 3]).map(lambda x: x * 2) # List([2, 4, 6])

List is immutable, so rather than mutating the list in place, the append and concat methods will return a new list with the given value appended to the end of the list. For example:

from monadic import List

List([1, 2, 3]).append(4) # List([1, 2, 3, 4])
List([1, 2, 3]).concat([4, 5, 6]) # List([1, 2, 3, 4, 5, 6])

The List type also supports the filter method, which will filter the list based on the given predicate. For example:

from monadic import List

List([1, 2, 3]).filter(lambda x: x % 2 == 0) # List([2])

The List type also supports the fold method, which will fold the list into a single value using the given function. For example:

from monadic import List

List([1, 2, 3]).fold(lambda acc, x: acc + x, 0) # 6

The List type is a type of Iterable. Other Iterable types provided by Monadic include Set and Dict, which have similar methods and semantics.

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

monadic-0.0.1.tar.gz (18.9 kB view details)

Uploaded Source

Built Distribution

monadic-0.0.1-py3-none-any.whl (16.3 kB view details)

Uploaded Python 3

File details

Details for the file monadic-0.0.1.tar.gz.

File metadata

  • Download URL: monadic-0.0.1.tar.gz
  • Upload date:
  • Size: 18.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/4.0.2 CPython/3.11.7

File hashes

Hashes for monadic-0.0.1.tar.gz
Algorithm Hash digest
SHA256 9f7e3924a77c154234348032a9b9351062a3e1d7163d83b9d2eeeef573e66de1
MD5 e346305296b15c4277cb6018240ad218
BLAKE2b-256 2fad245da554d7bd6670e049e596c785f2051c7b463c7f06d547a127a5c98b1f

See more details on using hashes here.

File details

Details for the file monadic-0.0.1-py3-none-any.whl.

File metadata

  • Download URL: monadic-0.0.1-py3-none-any.whl
  • Upload date:
  • Size: 16.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/4.0.2 CPython/3.11.7

File hashes

Hashes for monadic-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 0ddb385c659d311c80b6197be12d31e2506390d22fb6e0151e9f64dd9668c663
MD5 8858eaeb3c80269ff2e998034fd23fa6
BLAKE2b-256 99b3df17474c539737baeb52de9ee01ca8885bff113f6f0db2c9972eff043b97

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page