Giter Site home page Giter Site logo

dbrattli / expression Goto Github PK

View Code? Open in Web Editor NEW
423.0 35.0 30.0 2.24 MB

Pragmatic functional programming for Python inspired by F#

Home Page: https://expression.readthedocs.io

License: MIT License

Python 100.00%
functional-programming fsharp python railway-oriented-programming oslash

expression's Introduction

Expression

PyPI Python package Publish Package Documentation Status codecov

Pragmatic functional programming

Expression aims to be a solid, type-safe, pragmatic, and high performance library for frictionless and practical functional programming in Python 3.11+.

By pragmatic, we mean that the goal of the library is to use simple abstractions to enable you to do practical and productive functional programming in Python (instead of being a Monad tutorial).

Python is a multi-paradigm programming language that also supports functional programming constructs such as functions, higher-order functions, lambdas, and in many ways favors composition over inheritance.

Better Python with F#

Expression tries to make a better Python by providing several functional features inspired by F#. This serves several purposes:

  • Enable functional programming in a Pythonic way, i.e., make sure we are not over-abstracting things. Expression will not require purely functional programming as would a language like Haskell.
  • Everything you learn with Expression can also be used with F#. Learn F# by starting in a programming language they already know. Perhaps get inspired to also try out F# by itself.
  • Make it easier for F# developers to use Python when needed, and re-use many of the concepts and abstractions they already know and love.

Expression will enable you to work with Python using many of the same programming concepts and abstractions. This enables concepts such as Railway oriented programming (ROP) for better and predictable error handling. Pipelining for workflows, computational expressions, etc.

Expressions evaluate to a value. Statements do something.

F# is a functional programming language for .NET that is succinct (concise, readable, and type-safe) and kind of Pythonic. F# is in many ways very similar to Python, but F# can also do a lot of things better than Python:

  • Strongly typed, if it compiles it usually works making refactoring much safer. You can trust the type-system. With mypy or Pylance you often wonder who is right and who is wrong.
  • Type inference, the compiler deduces types during compilation
  • Expression based language

Getting Started

You can install the latest expression from PyPI by running pip (or pip3). Note that expression only works for Python 3.11+.

> pip install expression

To add Pydantic v2 support, install the pydantic extra:

> pip install expression[pydantic]

Goals

  • Industrial strength library for functional programming in Python.
  • The resulting code should look and feel like Python (PEP-8). We want to make a better Python, not some obscure DSL or academic Monad tutorial.
  • Provide pipelining and pipe friendly methods. Compose all the things!
  • Dot-chaining on objects as an alternative syntax to pipes.
  • Lower the cognitive load on the programmer by:
    • Avoid currying, not supported in Python by default and not a well known concept by Python programmers.
    • Avoid operator (|, >>, etc) overloading, this usually confuses more than it helps.
    • Avoid recursion. Recursion is not normally used in Python and any use of it should be hidden within the SDK.
  • Provide type-hints for all functions and methods.
  • Support PEP 634 and structural pattern matching.
  • Code must pass strict static type checking by pylance. Pylance is awesome, use it!
  • Pydantic friendly data types. Use Expression types as part of your Pydantic data model and (de)serialize to/from JSON.

Supported features

Expression will never provide you with all the features of F# and .NET. We are providing a few of the features we think are useful, and will add more on-demand as we go along.

  • Pipelining - for creating workflows.
  • Composition - for composing and creating new operators.
  • Fluent or Functional syntax, i.e., dot chain or pipeline operators.
  • Pattern Matching - an alternative flow control to if-elif-else.
  • Error Handling - Several error handling types.
    • Option - for optional stuff and better None handling.
    • Result - for better error handling and enables railway-oriented programming in Python.
    • Try - a simpler result type that pins the error to an Exception.
  • Collections - immutable collections.
    • TypedArray - a generic array type that abstracts the details of bytearray, array.array and list modules.
    • Sequence - a better itertools and fully compatible with Python iterables.
    • Block - a frozen and immutable list type.
    • Map - a frozen and immutable dictionary type.
    • AsyncSeq - Asynchronous iterables.
    • AsyncObservable - Asynchronous observables. Provided separately by aioreactive.
  • Data Modelling - sum and product types
    • @tagged_union - A tagged (discriminated) union type decorator.
  • Parser Combinators - A recursive decent string parser combinator library.
  • Effects: - lightweight computational expressions for Python. This is amazing stuff.
    • option - an optional world for working with optional values.
    • result - an error handling world for working with result values.
  • Mailbox Processor: for lock free programming using the Actor model.
  • Cancellation Token: for cancellation of asynchronous (and synchronous) workflows.
  • Disposable: For resource management.

Pipelining

Expression provides a pipe function similar to |> in F#. We don't want to overload any Python operators, e.g., | so pipe is a plain old function taking N-arguments, and will let you pipe a value through any number of functions.

from expression import pipe

v = 1
fn = lambda x: x + 1
gn = lambda x: x * 2

assert pipe(v, fn, gn) == gn(fn(v))

Expression objects (e.g., Some, Seq, Result) also have a pipe method, so you can dot chain pipelines directly on the object:

from expression import Some

v = Some(1)
fn = lambda x: x.map(lambda y: y + 1)
gn = lambda x: x.map(lambda y: y * 2)

assert v.pipe(fn, gn) == gn(fn(v))

So for example with sequences you may create sequence transforming pipelines:

from expression.collections import seq, Seq

# Since static type checkes aren't good good at inferring lambda types
mapper: Callable[[int], int] = lambda x: x * 10
predicate: Callable[[int], bool] = lambda x: x > 100
folder: Callable[[int, int], int] = lambda s, x: s + x

xs = Seq.of(9, 10, 11)
ys = xs.pipe(
    seq.map(mapper),
    seq.filter(predicate),
    seq.fold(folder, 0),
)

assert ys == 110

Composition

Functions may even be composed directly into custom operators:

from expression import compose
from expression.collections import seq, Seq

xs = Seq.of(9, 10, 11)
custom = compose(
    seq.map(lambda x: x * 10),
    seq.filter(lambda x: x > 100),
    seq.fold(lambda s, x: s + x, 0)
)
ys = custom(xs)

assert ys == 110

Fluent and Functional

Expression can be used both with a fluent or functional syntax (or both.)

Fluent syntax

The fluent syntax uses methods and is very compact. But it might get you into trouble for large pipelines since it's not a natural way of adding line breaks.

from expression.collections import Seq

xs = Seq.of(1, 2, 3)
ys = xs.map(lambda x: x * 100).filter(lambda x: x > 100).fold(lambda s, x: s + x, 0)

Note that fluent syntax is probably the better choice if you use mypy for type checking since mypy may have problems inferring types through larger pipelines.

Functional syntax

The functional syntax is a bit more verbose but you can easily add new operations on new lines. The functional syntax is great to use together with pylance/pyright.

from expression import pipe
from expression.collections import seq, Seq

xs = Seq.of(1, 2, 3)
ys = pipe(xs,
    seq.map(lambda x: x * 100),
    seq.filter(lambda x: x > 100),
    seq.fold(lambda s, x: s + x, 0),
)

Both fluent and functional syntax may be mixed and even pipe can be used fluently.

from expression.collections import seq, Seq
xs = Seq.of(1, 2, 3).pipe(seq.map(...))

Option

The Option type is used when a function or method cannot produce a meaningful output for a given input.

An option value may have a value of a given type, i.e., Some(value), or it might not have any meaningful value, i.e., Nothing.

from expression import Some, Nothing, Option

def keep_positive(a: int) -> Option[int]:
    if a > 0:
        return Some(a)

    return Nothing
from expression import Option, Ok
def exists(x : Option[int]) -> bool:
    match x:
        case Some(_):
            return True
    return False

Option as an effect

Effects in Expression is implemented as specially decorated coroutines (enhanced generators) using yield, yield from and return to consume or generate optional values:

from expression import effect, Some

@effect.option[int]()
def fn():
    x = yield 42
    y = yield from Some(43)

    return x + y

xs = fn()

This enables "railway oriented programming", e.g., if one part of the function yields from Nothing then the function is side-tracked (short-circuit) and the following statements will never be executed. The end result of the expression will be Nothing. Thus results from such an option decorated function can either be Ok(value) or Error(error_value).

from expression import effect, Some, Nothing

@effect.option[int]()
def fn():
    x = yield from Nothing # or a function returning Nothing

    # -- The rest of the function will never be executed --
    y = yield from Some(43)

    return x + y

xs = fn()
assert xs is Nothing

Option as an applicative

In functional programming, we sometimes want to combine two Option values into a new Option. However, this combination should only happen if both Options are Some. If either Option is None, the resulting value should also be None.

The map2 function allows us to achieve this behavior. It takes two Option values and a function as arguments. The function is applied only if both Options are Some, and the result becomes the new Some value. Otherwise, map2 returns None.

This approach ensures that our combined value reflects the presence or absence of data in the original Options.

from expression import Some, Nothing, Option
from operator import add

def keep_positive(a: int) -> Option[int]:
    if a > 0:
        return Some(a)
    else:
      return Nothing

def add_options(a: Option[int], b: Option[int]):
  return a.map2(add, b)

assert add_options(
  keep_positive(4),
  keep_positive(-2)
) is Nothing

assert add_options(
  keep_positive(3),
  keep_positive(2)
) == Some(5)

For more information about options:

Result

The Result[T, TError] type lets you write error-tolerant code that can be composed. A Result works similar to Option, but lets you define the value used for errors, e.g., an exception type or similar. This is great when you want to know why some operation failed (not just Nothing). This type serves the same purpose of an Either type where Left is used for the error condition and Right for a success value.

from expression import effect, Ok, Result

@effect.result[int, Exception]()
def fn():
    x = yield from Ok(42)
    y = yield from Ok(10)
    return x + y

xs = fn()
assert isinstance(xs, Result)

A simplified type called Try is also available. It's a result type that is pinned to Exception i.e., Result[TSource, Exception].

Sequence

Sequences is a thin wrapper on top of iterables and contains operations for working with Python iterables. Iterables are immutable by design, and perfectly suited for functional programming.

import functools
from expression import pipe
from expression.collections import seq

# Normal python way. Nested functions are hard to read since you need to
# start reading from the end of the expression.
xs = range(100)
ys = functools.reduce(lambda s, x: s + x, filter(lambda x: x > 100, map(lambda x: x * 10, xs)), 0)

# With Expression, you pipe the result, so it flows from one operator to the next:
zs = pipe(
    xs,
    seq.map(lambda x: x * 10),
    seq.filter(lambda x: x > 100),
    seq.fold(lambda s, x: s + x, 0),
)
assert ys == zs

Tagged Unions

Tagged Unions (aka discriminated unions) may look similar to normal Python Unions. But they are different in that the operands in a type union (A | B) are both types, while the cases in a tagged union type U = A | B are both constructors for the type U and are not types themselves. One consequence is that tagged unions can be nested in a way union types might not.

In Expression you make a tagged union by defining your type similar to a dataclass and decorate it with @tagged_union and add the appropriate generic types that this union represent for each case. Then you optionally define static or class-method constructors for creating each of the tagged union cases.

from dataclasses import dataclass
from expression import TaggedUnion, tag

@dataclass
class Rectangle:
    width: float
    length: float

@dataclass
class Circle:
    radius: float

@tagged_union
class Shape:
    tag: Literal["rectangle", "circle"] = tag()

    rectangle: Rectangle = case()
    circle: Circle = case()

    @staticmethod
    def Rectangle(width: float, length: float) -> Shape:
        """Optional static method for creating a tagged union case"""
        return Shape(rectangle=Rectangle(width, length))

    @staticmethod
    def Circle(radius: float) -> Shape:
        """Optional static method for creating a tagged union case"""
        return Shape(circle=Circle(radius))

Note that the tag field is optional, but recommended. If you don't specify a tag field then then it will be created for you, but static type checkers will not be able to type check correctly when pattern matching. The tag field if specified should be a literal type with all the possible values for the tag. This is used by static type checkers to check exhaustiveness of pattern matching.

Each case is given the case() field initializer. This is optioal, but recommended for static type checkers to work correctly. It's not required for the code to work properly,

Now you may pattern match the shape to get back the actual value:

    shape = Shape.Rectangle(2.3, 3.3)

    match shape:
        case Shape(tag="rectangle", rectangle=Rectangle(width=2.3)):
            assert shape.value.width == 2.3
        case _:
            assert False

Note that when matching keyword arguments, then the tag keyword argument must be specified for static type checkers to check exhaustiveness correctly. It's not required for the code to work properly, but it's recommended to avoid typing errors.

Notable differences between Expression and F#

In F# modules are capitalized, in Python they are lowercase (PEP-8). E.g in F# Option is both a module (OptionModule internally) and a type. In Python the module is option and the type is capitalized i.e Option.

Thus in Expression you use option as the module to access module functions such as option.map and the name Option for the type itself.

>>> from expression import Option, option
>>> Option
<class 'expression.core.option.Option'>
>>> option
<module 'expression.core.option' from '/Users/dbrattli/Developer/Github/Expression/expression/core/option.py'>

Common Gotchas and Pitfalls

A list of common problems and how you may solve it:

Expression is missing the function/operator I need

Remember that everything is just a function, so you can easily implement a custom function yourself and use it with Expression. If you think the function is also usable for others, then please open a PR to include it with Expression.

Resources and References

A collection of resources that were used as reference and inspiration for creating this library.

How-to Contribute

You are very welcome to contribute with suggestions or PRs ๐Ÿ˜ It is nice if you can try to align the code and naming with F# modules, functions, and documentation if possible. But submit a PR even if you should feel unsure.

Code, doc-strings, and comments should also follow the Google Python Style Guide.

Code checks are done using

To run code checks on changed files every time you commit, install the pre-commit hooks by running:

> pre-commit install

Code of Conduct

This project follows https://www.contributor-covenant.org, see our Code of Conduct.

License

MIT, see LICENSE.

expression's People

Contributors

6293 avatar brendanmaguire avatar bruggsy avatar dbrattli avatar denghz avatar erlendvollset avatar exyi avatar garyrob avatar ghisvail avatar phi-friday avatar philvarner avatar renovate[bot] avatar shalokshalom avatar tobricz avatar u3kkasha avatar yuuuxt avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

expression's Issues

Question: How do you use @effect.result with asyncio?

i'm unsure of how to use async functions and @effect.result. The below results in tons of typehints

  @effect.result[bool, LunaException]()
  async def create_collection(self, params: CreateCollectionParams):
    # Create a collection in Qdrant
    result = await self.connection.ok.create_collection(
      collection_name=params.collection_name, vectors_config=params.dense_vector_params, sparse_vectors_config=params.sparse_vector_params
    )
    return Ok(result)
Argument of type "(self: Self@VectordbClient, params: CreateCollectionParams) -> Coroutine[Any, Any, Result[bool, Any]]" cannot be assigned to parameter "fn" of type "(**_P@__call__) -> (Generator[bool | None, bool, bool | None] | Generator[bool | None, None, bool | None])" in function "__call__"
  Type "(self: Self@VectordbClient, params: CreateCollectionParams) -> Coroutine[Any, Any, Result[bool, Any]]" is incompatible with type "(**_P@__call__) -> (Generator[bool | None, bool, bool | None] | Generator[bool | None, None, bool | None])"
    Function return type "Coroutine[Any, Any, Result[bool, Any]]" is incompatible with type "Generator[bool | None, bool, bool | None] | Generator[bool | None, None, bool | None]"

README unclear about where 'seq' comes from

This looks like an impressive project and I'm definitely looking at it for a health IT project, but I have a question, and maybe this can simulate a fresh look at the docs:

My first noobie reaction to this was, "where does 'seq' come from?" and to scroll up and it's never mentioned before here, and not explicitly defined.

ys = xs.pipe(
    seq.map(lambda x: x * 10),
    seq.filter(lambda x: x > 100),
    seq.fold(lambda s, x: s + x, 0)
)

No big deal, but the README is the main entrypoint to the project for noobs like me
Thank you for your work and thank you for taking the time to read this

Pydantic schema cannot validate self defined types

Bug Description
Pydantic schema fails to validate user-defined types.

Expected Behavior
The Pydantic schema should successfully validate user-defined types.

Code Example
Below is a minimal code example to illustrate the issue:

from typing import Any
from pydantic import BaseModel, Field
from pydantic_core import CoreSchema, GetCoreSchemaHandler
from typing_extensions import Annotated

PositiveInt = Annotated[int, Field(gt=0)]

class Username(str):
    @classmethod
    def __get_pydantic_core_schema__(
            cls, source_type: Any, handler: GetCoreSchemaHandler
    ) -> CoreSchema:
        return core_schema.no_info_after_validator_function(cls, handler(str))

class Model(BaseModel):
    annotated_type: Option[PositiveInt] = Nothing
    annotated_type_none: Option[PositiveInt] = Nothing

    custom_type: Option[Username] = Nothing
    custom_type_none: Option[Username] = Nothing

obj = dict(
    annotated_type=20,
    annotated_type_none=None,
    custom_type='test_user',
    custom_type_none=None
)

model = Model.model_validate(obj)

Additional Context

  • The is_instance_schema function does not work for annotated types.
  • A potential solution could be to either remove the annotations or avoid checking is_instance since the validator of the inner type T of Option[T] should validate the value itself.

Environment

  • Pydantic version: latest

Thank you!

In fact, I have been admiring the scala-style chaining function call and other functional programing concepts for a long time, but my usual work is all on python, Now I find I can achieve a good trade-off by combining your library and fn(used for simplifng lambda function into a blank space, sadly not maintained since 2014), Thank you again! Hope you can keep maintain and enpower this fantanstic object!

_TSource in Option should be covariant

Describe the bug
_TSource type in Option should be covariant.

class A:
  pass
class B(A):
  pass

x: Option[A] = Some(B())

Now since the _TSource is invariant, pyright will complain that B is not the same as A, but actually Option type should be covariant. Is Some[Dog] a Some[Animal]? I think you'll agree that the answer is yes. Also Option type is not actually mutable, so there is no way to assign a Some[Dog] to a variable typed Some[Aninmal] and then make it become Some[Cat]

Additional context
Add any other context about the problem here.

  • OS Linux
  • Expression version 4.2.4
  • Python version 3.11

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Other Branches

These updates are pending. To force PRs open, click the checkbox below.

  • chore(deps): update dependency sphinx-autodoc-typehints to v1.24.0

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Ignored or Blocked

These are blocked by an existing closed PR and will not be recreated unless you click a checkbox below.

Detected dependencies

github-actions
.github/workflows/python-package.yml
  • actions/checkout v3
  • actions/setup-python v4
.github/workflows/python-publish.yml
  • actions/checkout v3
  • actions/setup-python v4
pip_requirements
docs/requirements.txt
  • sphinx-autodoc-typehints >=1.17.0
  • expression >=2.0.1
poetry
pyproject.toml
  • typing-extensions ^4.1.1
  • pytest-asyncio ^0.21.0
  • pytest ^7.2.0
  • coverage ^6.4.3
  • black ^23.0.0
  • isort ^5.10.1
  • flake8 ^6.0.0
  • coveralls ^3.3.1
  • pre-commit ^3.0.0
  • autoflake ^2.0
  • dunamai ^1.12.0
  • hypothesis ^6.54.2
  • jupyter-book ^0.15.0
  • sphinx-autodoc-typehints ^1.17.0
  • pydantic ^1.10.0

  • Check this box to trigger a request for Renovate to run again on this repository

Support for Either type

Is your feature request related to a problem? Please describe.

Coming from a bit of Haskell and more Scala, I was surprised to see there was no Either type. I'd like to see this added.

Describe the solution you'd like

I'd like to see the library have an Either type.

Describe alternatives you've considered

I'm aware that this library is supposed to be a more direct implementation of F# in Python than every type from other languages. Either is a pretty common concept (e.g., in Haskell and Scala), even if in Scala, Try is more frequently used. A lot of uses of Either will use Result or Try instead, but I think it would be good to have a generic Either for completeness, even if it's not part of F#.

Additional context

n/a

Logo

Expression needs a logo. Anyone that wants to contribute to this great project?

How are the collections implemented?

I see that you provide different collections with immutability in mind.

Are they also implemented as immutable data structures?

If not, I recommend adding that info to the README section, that cares about the differences to FSharp.

In that case, I do suggest this as feature. :)

If it does, I recommend adding that info to the README as well.

Issue on page /tutorial/effects.html

I am trying to reproduce the following code block from this tutorial:

from expression import effect
from expression.core import option, Option, Some, Nothing

def divide(a: float, divisor: float) -> Option[int]:
    try:
        return Some(a/divisor)
    except ZeroDivisionError:
        return Nothing


@effect.option
def comp(x):
    result = yield from divide(42, x)
    result += 32
    print(f"The result is {result}")
    return result

comp(42)

It returns the following on jupyter lab:

TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_477668/4077434771.py in <cell line: 11>()
     10 
     11 @effect.option
---> 12 def comp(x):
     13     result = yield from divide(42, x)
     14     result += 32

TypeError: OptionBuilder() takes no arguments

I couldn't make this code to work. Any suggestions?

How about using TypeAlias with Result and Option?

Is your feature request related to a problem? Please describe.
I often use method "is_ok" and "is_error", but unfortunately, pylance does not recognize it

import typing as tp

import expression as ex


def get_result() -> ex.Result[int, tp.Any]:
    return ex.Ok(1)


a = get_result()
if a.is_ok():
    b = a  # in pylance:: (variable) b: Result[int, Any]
    # in pylance:: Cannot access member "value" for type "Result[int, Any]"
    #   Member "value" is unknown
    v = a.value
else:
    b = a  # in pylance:: (variable) b: Result[int, Any]
    # in pylance:: Cannot access member "error" for type "Result[int, Any]"
    #   Member "error" is unknown
    v = a.error

So I'm replacing it with "isinstance", but it's still a little disappointing

import typing as tp

import expression as ex


def get_result() -> ex.Result[int, tp.Any]:
    return ex.Ok(1)


a = get_result()
if isinstance(a, ex.Ok):
    b = a  # in pylance:: (variable) b: Ok[int, Any]
    v = a.value  # in pylance:: (variable) v: int
else:
    b = a  # in pylance:: (variable) b: Result[int, Any]
    # in pylance:: Cannot access member "error" for type "Result[int, Any]"
    #   Member "error" is unknown
    v = a.error

c = get_result()
if isinstance(c, ex.Error):
    d = c  # in pylance:: (variable) d: Error[int, Any]
    v = c.error  # in pylance:: (variable) v: Any
else:
    d = c  # in pylance:: (variable) d: Result[int, Any]
    # in pylance:: Cannot access member "value" for type "Result[int, Any]"
    #   Member "value" is unknown
    v = c.value

Describe the solution you'd like

It looks better when using TypeAlias

import typing as tp

import expression as ex
import typing_extensions as tpe

TValue = tp.TypeVar("TValue")
TError = tp.TypeVar("TError")

TypedResult: tpe.TypeAlias = tp.Union[ex.Ok[TValue, TError], ex.Error[TValue, TError]]


def get_typed_result() -> TypedResult[int, tp.Any]:
    return ex.Ok(1)


a2 = get_typed_result()
if isinstance(a2, ex.Ok):
    b2 = a2  # in pylance:: (variable) b2: Ok[int, Any]
    v2 = a2.value  # in pylance:: (variable) v2: int
else:
    b2 = a2  # in pylance:: (variable) b2: Error[int, Any]
    v2 = a2.error  # in pylance:: (variable) v2: Any

c2 = get_typed_result()
if isinstance(c2, ex.Error):
    d2 = c2  # in pylance:: (variable) d2: Error[int, Any]
    v2 = c2.error  # in pylance:: (variable) v2: Any
else:
    d2 = c2  # in pylance:: (variable) d2: Ok[int, Any]
    v2 = c2.value  # in pylance:: (variable) v2: int

If Result and Option were used simply to define the interface, I think its could be easily replaced. But I'm not sure, so I can't pr and just suggest it.

Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.

Additional context
Python 3.10.9
vscode 1.74.3
pylance 2023.1.40

Map change drops current items when len is two

When adding a new item to a Map having 2 items, using change method adds the new item but drops the items it had.

Map.empty().change(1, lambda _: Some(1)).change(2, lambda _: Some(2)).change(3, lambda _: Some(3))

A Map having 3 items is expected, but actually the resulting Map only has (3, 3).

image

  • OS [MacOS]
  • Expression version [4.2.2]
  • Python version [3.11.1]

Seq and generators

Hi!

Is there a way to map over generators? Can seq help? For example:

YEARS:List[int] = [2015, 2016, 2017, 2018, 2019, 2020, 2021]

def get_train_test_years():
    for i in range(3, len(YEARS)):
        yield YEARS[:i], YEARS[i]

def transform(x,y):
    return x+"Arbitrary complex",y+"Arbitrary complex"

#Does not work
ys = pipe(get_train_test_years(),
    seq.map(transform)
)

# works but repetitive
test_train_candidates  = (transform(train_years, test_year) for train_years, test_year in get_train_test_years())

Thanks!

Fishy locking in CancellationSource

Describe the bug
Usage of _lock in https://github.com/cognitedata/Expression/blob/main/expression/system/cancellation.py doesn't look correct.

I was looking through files and got puzzled by how CancellationTokenSource.dispose() is implemented.

Apparently it tries to read _is_disposed under the lock but on the next like assigns it without lock. It also calls listeners without lock.

On the other hand register_internal seems to do the opposite: it reads _is_disposed without lock and accesses _listeners inside a critical section.

To be strict all access to variables should be protected and protected consistently. E.g. if it's dangerous to call a callback under the lock - a copy (slice) of _listeners could be made (in the same critical section with checking for _is_disposed).

Type aliases using Union syntax incompatible with Python 3.9

Describe the bug
A basic Python script bombs when importing from expression.collections.

To Reproduce

  1. Install expression into Python 3.9 virtual environment using pip install.
  2. Create a basic top-level script that imports expression.collections.
  3. Run script with Python 3.9 from virtual environment.

Expected behavior
Python script runs without issue.

Code or Screenshots
Here is the code from my Python script:

from expression.collections import seq, Seq

if __name__ == '__main__':
    print("All done!")

Here is the traceback I get:

Traceback (most recent call last):
  File "/home/atonally/develop/sandbox-py39/main.py", line 3, in <module>
    from expression.collections import seq, Seq
  File "/home/atonally/develop/sandbox-py39/.venv/lib/python3.9/site-packages/expression/__init__.py", line 11, in <module>
    from . import collections, core, effect
  File "/home/atonally/develop/sandbox-py39/.venv/lib/python3.9/site-packages/expression/collections/__init__.py", line 4, in <module>
    from . import array, asyncseq, block, map, seq
  File "/home/atonally/develop/sandbox-py39/.venv/lib/python3.9/site-packages/expression/collections/array.py", line 28, in <module>
    from expression.core import (
  File "/home/atonally/develop/sandbox-py39/.venv/lib/python3.9/site-packages/expression/core/__init__.py", line 20, in <module>
    from .fn import TailCall, TailCallResult, tailrec, tailrec_async
  File "/home/atonally/develop/sandbox-py39/.venv/lib/python3.9/site-packages/expression/core/fn.py", line 24, in <module>
    TailCallResult = _TResult | TailCall[_P]
TypeError: unsupported operand type(s) for |: 'TypeVar' and '_GenericAlias'

Additional context

  • Kubuntu 22.04
  • Expression 4.2.2
  • Python 3.9.16

Pipeline

I understand your decision, to not use operator overloading.
To frame my following opposition to it, here my background:

I can not use FSharp in my current project, since it is barely implemented.

Python is an alternative. Something like Coconut is awesome, but its editor support is limited and I like good syntax highlighting.

After looking for a good hour through all kinds of Stackoverflow posts, PyPi, and several other sources to find at least a proper pipe, did I finally stumble over your project and was finally in Aww O.O

I finally found it, and then I read this:

Screenshot_2020-10-11-11-14-58-81

Just to be honest: This is how F# code looks like:

Screenshot_2020-10-11-11-36-31-62

I dont see any sense in a FSharp library for Python, that does not allow me to do that.

I can not emphasis enough, how important the pipeline operator is for me, its just fundamental to how FSharp feels and works for me. :)

Especially to place them into a new line, which is how idiomatic FSharp looks and feels.

Its also easier, to apply tutorials.

I understand that |> is probably not possible without recompiling and that overloading an already used operator is confusing, while I strongly suggest to adopt one that is not in broader use, or give the option to use | or >> anyway

I really see the point of avoiding confusion, while I dont work in a team - like most coders, especially in the open-source field - and reading into code based is always a struggle and this makes code at least more unambiguous and it's important to me, to be able to use FSharps traditional method.

It confuses me much more, to do it the way you implemented it.

Thanks a lot!

Adding type annotations to curried functions

From what I can see in core/curry.py, there's no way to specify type annotations on-the-fly in a meaningful way. However, instead of recommending against usage, is it worth providing a @functools.wraps decorator over the called function so some notion of annotation and docstring is preserved?

It trusts that the user knows what they're doing, of course :)

Improve the ergonomics of pipelines

Is your feature request related to a problem? Please describe.
In F# one can write the following pipeline:

let result =
  (val1, val2, val3)
  |> convertVal1  // A * B * C -> (P * Q) * B * C
  |> convertVal2  // (P * Q) * B * C -> S * C
  |> convertVal3  // S * C -> S * T

With Expression I was able to implement it in two non-ergonomic ways:

result = pipe(
    (val1, val2, val3),
    lambda x: starpipe(x, convertVal1),
    lambda x: starpipe(x, convertVal2),
    lambda x: starpipe(x, convertVal3)
)

result = starpipe(
    starpipe(
        starpipe(
            (val1, val2, val3),
            convertVal1
        ),
        convertVal2
    ),
    convertVal3
)

Describe the solution you'd like
I would like to write the same code as in F#.

Describe alternatives you've considered
Perhaps I am missing something and Expression does allow writing something similar to the F# code.

Additional context

Seq.map, TypeError: map() missing 1 required positional argument: 'mapper'

Having some background in F# and just a beginner in Python, thought Expression lib worth exploring.

Using Ref https://cognitedata.github.io/Expression/expression/collections/seq.html and trying something as simple as:

    from expression.collections import Seq
    xs = Seq([1, 2, 3])
    ys = xs.pipe(
        Seq.map(lambda x: x + 1),
        Seq.filter(lambda x: x < 3)
    )

resulting in error:

  File "<ipython-input-54-c73530b41e79>", line 5, in <module>
      Seq.map(lambda x: x + 1),
  TypeError: map() missing 1 required positional argument: 'mapper'

Any help?

Other F# default Seq functions

The BCL F# Seq module has some other really helpful functions, do you have plans to implement them? need help/accept PR?

average
averageBy
cache
countBy
distinct
distinctBy
exactlyOne
exists
exists2
forall
forall2
groupBy
iteri
last
maxBy
minBy
nth
pairwise
pick
reduce
skipWhile
sort
sortBy
takeWhile
truncate
tryFind
tryFindIndex
tryPick
windowed
zip3

4.2.0 has invalid python 3.9 syntax

Describe the bug
https://github.com/cognitedata/Expression/blob/19637ad4b033dc7a454ef7c1bd7164743b782334/expression/core/result.py#L199 uses a match syntax that's only available in python >= 3.10, but the expression package is marked as compatible with python >= 3.9.

To Reproduce

$ python3 --version
Python 3.9.14
$ mkdir /tmp/repro \
      && cd /tmp/repro \
      && python3 -m venv .venv \
      && source .venv/bin/activate
$ pip install expression==4.2.0
$ echo "import expression" >> repro.py
$ python3 repro.py
Traceback (most recent call last):
  File "/private/tmp/repro/repro.py", line 1, in <module>
    import expression
  File "/private/tmp/repro/.venv/lib/python3.9/site-packages/expression/__init__.py", line 11, in <module>
    from . import collections, core, effect
  File "/private/tmp/repro/.venv/lib/python3.9/site-packages/expression/collections/__init__.py", line 4, in <module>
    from . import array, asyncseq, block, map, seq
  File "/private/tmp/repro/.venv/lib/python3.9/site-packages/expression/collections/array.py", line 28, in <module>
    from expression.core import (
  File "/private/tmp/repro/.venv/lib/python3.9/site-packages/expression/core/__init__.py", line 5, in <module>
    from . import aiotools, option, result
  File "/private/tmp/repro/.venv/lib/python3.9/site-packages/expression/core/result.py", line 199
    match other:
          ^
SyntaxError: invalid syntax

And thank you for all your work maintaining this package. :)

Cannot pickle any tagged_union


Bug Description
Pickling tagge_union type is not working because __dataclass_fields__ is set on the object's __dict__ instead of the class __dict__. Additionally, __dataclass_fields__ cannot be pickled automatically since it contains a mappingproxy.

Expected Behavior
The Option type should be pickled successfully without errors.

Code Example
Below is a minimal code example to illustrate the issue:

import pickle
from expression import Some

obj = Some(1)

# Attempt to pickle the object
try:
    pickled_obj = pickle.dumps(obj)
    unpickled_obj = pickle.loads(pickled_obj)
    print("Pickling and unpickling successful.")
except Exception as e:
    print(f"Error during pickling: {e}")

Additional Context

  • The issue arises because __dataclass_fields__ is set on the instance's __dict__ rather than the class's __dict__.
  • __dataclass_field__ contains a mappingproxy, which cannot be pickled automatically.

Environment

  • Pydantic version: 5.0.2

Error Message
If applicable, include the error message you encountered:

Error during pickling: can't pickle 'mappingproxy' object

Potential Solution
Implement __setstate__ and __getstate__

change function for map does not appear to work correctly

Describe the bug
This may not be a bug, but I can't find documentation for the change function / method as it applies to maps. It certainly does not appear to operate in a way that I would expect.

Calling change on an existing, nonempty map appears to operate as if it were called on an empty map.

To Reproduce

This results in a Nothing_ error, when I would expect it to return the map unchanged:

from expression.collections import Map

xs = Map.of(**dict(a=10, b=20))
xs.change("a", lambda x: x)

This results in map [("a", 1)] when I would expect map [("a", 1); ("b", 20)]:

xs.change("a", lambda _: Some(1))

Expected behavior
As indicated above, I would expect that the first block would return the map unchanged, and that the second would only modify the item with the key "a".

Additional context
Add any other context about the problem here.

  • OS MacOS 13
  • Expression version 4.2.2
  • Python version 3.11.1

Explore better typing for comprehensions / computational expressions

Consider:

    @effect.option
    def fn() -> Generator[Any, Any, List[str]]:
        x: int = yield 42
        y: str = yield f"{x} as a string"
        z: List[str] = yield from Some([y, str(x)])
        return z

Currently, unless each expression has the same type, you're stuck either being untyped or Any-typed and manually typing each bound name. This is really unfortunate and not at all type-safe.

Chaining a bunch of transformations of values within a functor or monad is a very common use of Haskell do-expressions, Scala for-expressions, and presumably (although i have no first hand knowledge) F# computational expressions.

It's really nice to be able to chain these and rely on types to ensure the correctness of each step.

With the current approach, it doesn't seem possible to type the individual layers of unwrapping differently unless they all share the same contained type (for the contravariant type parameter). This is really limiting the usefulness, alas.

Context: I'm trying to introduce some good foundations and abstractions for correctness at my company, and I think Expression could be a part of this based on its trajectory. However, the current limitations/ergonomics like this, combined with limitations in mypy would make this a somewhat difficult sell, so I'm hoping there's a better approach or some ideas for improvement. Happy to assist where possible, but i'm definitely not an expert in python or python typing.

Actor Model & Railway oriented programming

Mailbox Processor: for lock free programming using the Actor model.
Cancellation Token: for cancellation of asynchronous (and synchronous) workflows.
Disposable: For resource management.

Can you provide an example of how to use the Actor model with railway-oriented programming?

some_actor = ...

async def runner():
    return pipe(
        Ok(1),
        func1,
        func2,
        lambda x: some_actor.post(x),
        func3,
    )

Is the above code a suggested usage?

NonEmptyList

Is your feature request related to a problem? Please describe.
I have found NonEmptyList from the Scala Cats library very useful in the past. I'd like to implement a similar collection type in Expression. Is this something you would be open to @dbrattli ?

Describe the solution you'd like
Just a few examples outlined below. Overall I'd see it working in a similar way to expression.collections.Block (i.e. an immutable collection).

list(NonEmptyList.one(1)) == [1]

list(NonEmptyList.of(1, 2, 3)) == [1, 2, 3]

NonEmptyList.from([1, 2, 3]).map(list) == Some([1, 2, 3])

NonEmptyList.from([]) == Nothing

def do_something_with_list_that_must_be_non_empty(l: NonEmptyList):
    # do things with elements in `l`

Describe alternatives you've considered
Doing this everywhere in your code:

def do_something_with_list_that_must_be_non_empty(l: list):
    if l:
        # do things with elements in `l`

Generic monad algorithm

Is it possible to define a function that can handle multiple types of effects depends on the object that yields the effect?
Like in Haskell

join :: Monad m => m (m a) -> m a
join xss = 
    do xs <- xss
       xs                            

which would work for all monads including, list, maybe, io, stm, etc...
I can help about it if needed.

Is there some helper function to use parameters for function in Result?

Is your feature request related to a problem? Please describe.
There are times when I have to call a function in an uncertain state, and I use it in the following way.

import expression as ex


def func(a: int, b: str):
    return [a, b]


def get_func():
    return ex.Ok(func)


func_result = get_func()
value = func_result.map(lambda f: f(1, "test"))

It doesn't matter if it's a simple case.
But, if it overlaps a little bit, it's quite a hassle.

Describe the solution you'd like
I think there are two ways

import expression as ex


def func(a: int, b: str):
    return [a, b]


def get_func():
    return ex.Ok(func)


func_result = get_func()
value = helper_func(func_result, 1, "asd")

or

import typing as tp
import expression as ex


def func(a: int, b: str, c: ex.Result[int, tp.Any]):
    return [a, b]


def get_func():
    return ex.Ok(func)


func_result = get_func()

value = ex.pipe(
    func_result,
    func_helper.map(1),
    func_helper.map("asd"),
    func_helper.bind(123),
)

Describe alternatives you've considered
I'm using the second method as follows, but I don't think it's pythonic.

do not use below, too many bugs.

from functools import partial
from typing import Any, Callable, Union

from expression import Ok, Result, pipe
from expression.extra.result import catch
from typing_extensions import Concatenate, ParamSpec, TypeVar

ArgT = TypeVar("ArgT")
ParamT = ParamSpec("ParamT")
ResultT = TypeVar("ResultT")


class Currylike:
    @classmethod
    def map(
        cls, arg: ArgT
    ) -> Callable[
        [
            Union[
                Callable[Concatenate[ArgT, ParamT], ResultT],
                Result[Callable[Concatenate[ArgT, ParamT], ResultT], Any],
            ]
        ],
        Result[Callable[ParamT, ResultT], Any],
    ]:
        return partial(cls.fmap, arg)

    @classmethod
    def bind(
        cls, arg: ArgT
    ) -> Callable[
        [
            Union[
                Callable[Concatenate[Result[ArgT, Any], ParamT], ResultT],
                Result[Callable[Concatenate[Result[ArgT, Any], ParamT], ResultT], Any],
            ]
        ],
        Result[Callable[ParamT, ResultT], Any],
    ]:
        return partial(cls.fbind, arg)

    @staticmethod
    def fmap(
        arg: Union[ArgT, Result[ArgT, Any]],
        func: Union[
            Callable[Concatenate[ArgT, ParamT], ResultT],
            Result[Callable[Concatenate[ArgT, ParamT], ResultT], Any],
        ],
    ) -> Result[Callable[ParamT, ResultT], Any]:
        if isinstance(arg, Result):
            if isinstance(func, Result):
                return func.bind(lambda f: arg.map(lambda value: partial(f, value)))  # type: ignore

            return arg.map(lambda value: partial(func, value))  # type: ignore

        if isinstance(func, Result):
            return func.map(lambda f: partial(f, arg))  # type: ignore
        return Ok(partial(func, arg))  # type: ignore

    @staticmethod
    def fbind(
        arg: Union[ArgT, Result[ArgT, Any]],
        func: Union[
            Callable[Concatenate[Result[ArgT, Any], ParamT], ResultT],
            Result[Callable[Concatenate[Result[ArgT, Any], ParamT], ResultT], Any],
        ],
    ) -> Result[Callable[ParamT, ResultT], Any]:
        if isinstance(arg, Result):
            if isinstance(func, Result):
                return func.map(lambda f: partial(f, arg))  # type: ignore
            return Ok(partial(func, arg))  # type: ignore

        if isinstance(func, Result):
            return func.map(lambda f: partial(f, Ok(arg)))  # type: ignore
        return Ok(partial(func, Ok(arg)))  # type: ignore

    @staticmethod
    def fcall(
        func: Result[Callable[ParamT, ResultT], Any],
        *args: ParamT.args,
        **kwargs: ParamT.kwargs
    ) -> Result[ResultT, Any]:
        return func.bind(lambda f: catch(exception=Exception)(f)(*args, **kwargs))

def func(a: int, b: str, c: ex.Result[int, tp.Any]):
    return [a, b]


def get_func():
    return Ok(func)


func_result = get_func()

value = pipe(
    func_result,
    Currylike.map(1),
    Currylike.map("asd"),
    Currylike.bind(123),
    Currylike.fcall
)

I would appreciate it if you could let me know if there is a good method that you have already provided.

Additional context
Add any other context or screenshots about the feature request here.

Short-circuit does not work for me in pipe()

Describe the bug
Short-circuit does not work for me in pipe().
Maybe i'm using this function in a wrong way. If so, please tell me how it should be.

Thank you.

To Reproduce
Execute this code with Expression 2.0.0:

from expression import pipe, effect, Ok
from expression.core.option import Nothing


@effect.result[int, Exception]()
def mulby10(x):
    yield from Ok(x * 10)
 
@effect.option[int]()
def divbyzero(x):
    try:
        yield from Ok(x / 0)
    except Exception as exn:
        yield from Nothing

def main():
    v = 1
    res = pipe(
        v,
        divbyzero,
        mulby10
        )
    print(f"{res=}")

if __name__ == "__main__":
    main()

It executes mulby10 after divbyzero returns Nothing and shows error:

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/Users/davaeron/repos/automation/automation/cli.py", line 18, in main
    res = pipe(
  File "/Users/davaeron/repos/automation/__pypackages__/3.10/lib/expression/core/pipe.py", line 136, in pipe
    return compose(*fns)(value)
  File "/Users/davaeron/repos/automation/__pypackages__/3.10/lib/expression/core/compose.py", line 136, in _compose
    return reduce(lambda acc, f: f(acc), fns, source)
  File "/Users/davaeron/repos/automation/__pypackages__/3.10/lib/expression/core/compose.py", line 136, in <lambda>
    return reduce(lambda acc, f: f(acc), fns, source)
  File "/Users/davaeron/repos/automation/__pypackages__/3.10/lib/expression/core/builder.py", line 96, in wrapper
    result = self._send(gen, done)
  File "/Users/davaeron/repos/automation/__pypackages__/3.10/lib/expression/core/builder.py", line 52, in _send
    yielded = gen.send(value)
  File "/Users/davaeron/repos/automation/automation/cli.py", line 7, in mulby10
    yield from Ok(x * 10)
TypeError: unsupported operand type(s) for *: 'Nothing_' and 'int'

Expected behavior
res=Nothing after divbyzero function, mulby10 should be unexecuted

Additional context

  • OS MacOS 12.4
  • Expression version: 2.0.0
  • Python version: 3.10.4

Possible wrong order of parent classes in Error?

Just noticed this playing with Try (Python 3.9.7, expression 1.1.0):

Python 3.9.7 (default, Aug 31 2021, 13:28:12) 
[GCC 11.1.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> python.el: native completion setup loaded
>>> from expression import Failure, Success
>>> x = Success(10)
>>> y = Failure(ValueError())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/vputz/.cache/pypoetry/virtualenvs/plugin-playground-TQ0vVnN3-py3.9/lib/python3.9/site-packages/expression/core/result.py", line 199, in __init__
    super().__init__(str(error))
TypeError: object.__init__() takes exactly one argument (the instance to initialize)

Looks like in result.py:199:


class Error(Result[TSource, TError], ResultException):
    """The Error result case class."""

    def __init__(self, error: TError) -> None:
        super().__init__(str(error))
        self._error = error

... it looks like it's trying to call the Result version of __init__ first (which makes sense; it's first in the parent list) instead of Error.__init__, which indeed takes a string as an argument.

Changing the order of parents to:

class Error(ResultException, Result[TSource, TError]):

Avoids the error:

>>> from expression import Failure, Success
>>> x = Success(10)
>>> y = Failure(ValueError())

...although there may be another way to resolve it which may be preferable.

Nothing_.__iter__ method not typed correctly?

The example from the tutorial does not type check with Pylance:

from expression import effect, Some, Nothing

@effect.option
def fn():
    x = yield from Nothing # or a function returning Nothing

    # -- The rest of the function will never be executed --
    y = yield from Some(43)

    return x + y

xs = fn()
assert xs is Nothing

Type checking fails at x = yield from Nothing -- Pylance thinks it is Unknown type. However, if I change the return type of Nothing_.__iter__ to be like the return type from Some:

    def __iter__(self) -> Generator[TSource, TSource, TSource]:

Then x = yield from Nothing type checks to an Any. Which, given the type of Nothing seems correct. Though of course nothing will yield from Nothing because of the raise Nothing in __iter__.

However, I don't know if changing the return type to Generator has negative consequences in other contexts.

Automatically catch and wrap exceptions thrown in @effect.result functions?

Currently, it looks like any non-Expression exceptions thrown in a effect.result decorated function is re-raised by the decorator. I'm wondering if it might be useful to have the decorator return an Error object instead, wrapping the raised Exception object?

This would reduce the amount of manual conversions from raised exceptions to Error objects, enabling easier interfacing with code/libraries that throw exceptions.

Thoughts? Happy to take a stab at implementing this. Either way, I'm digging this library!

__str__ and __repr__ of Failure & Success

Describe the bug
When using str or repr on Try, it prints the representations of the parent class

To Reproduce

from expression import Try, Success, Failure
print( Success(8) )
print( Failure(8) )

Output:
Ok 8
Error 8

Expected behavior
Output:
Success 8
Failure 8

Additional context

  • OS [e.g. WSL 2]
  • Expression version [e.g 4.3.0]
  • Python version [e.g. 3.10]

Try docs

Not sure if this is intentional, but the docs for the Try class are nearly identical to the docs for the Result class.

I would expect the source code reference for Try to show a type alias (Try = Result[TSource, Exception]) or sub-class definition.

https://cognitedata.github.io/Expression/expression/

seq.map does not type check correctly

Describe the bug

The functional pipe examples where seq.map is the first function in the pipe causes a type error.

To Reproduce
Steps to reproduce the behavior:

Screenshot 2023-07-26 at 17 58 18

Expected behavior

pyright type checks correctly

Code or Screenshots

This causes type error:

from expression.collections import seq, Seq

xs = Seq.of(9, 10, 11)
ys = xs.pipe(
    seq.map(lambda x: x * 10),
    seq.filter(lambda x: x > 100),
    seq.fold(lambda s, x: s + x, 0)
)

This passes:

from expression.collections import seq, Seq

xs = Seq.of(9, 10, 11)
ys = xs.pipe(
    seq.filter(lambda x: x > 100),
    seq.map(lambda x: x * 10),
    seq.fold(lambda s, x: s + x, 0)
)

Additional context
Add any other context about the problem here.

  • MacOS
  • Expression version 4.2.4
  • Tested on Python 3.11.4 and 3.10.12

@effect.result decorator return type

I've started plumbing the Try type throughout a program I'm working on for better error handling :)

I just realized that the @result decorator drops type information (by returning a ResultBuilder[Any, Any]).

I'm not sure if a decorator function could be written such that the type checker will infer the resulting (decorated) function's return type based on the input function's return type, which would be ideal. It seems like that should be possible, since the function-to-be-decorated (and its type annotation) will be available to the decorator function, but I haven't tried it.

But even if that's not possible, I'd rather pass type information around explicitly than drop it (via Any).

By defining this try_of helper, I can (with a bit of boilerplate) recover the missing type information.

@effect.result
def surprise() -> Try[List[str]]:
    yield from Success(["Good", "luck", "!"])


def try_of(_: Type[T]) -> ResultBuilder[T, Exception]:
    return effect.result


@try_of(List[str])
def list_of_str() -> Try[List[str]]:
    yield from Success(["well", "typed", ":)"])


# not sure if I love it, but this seems to work, too
@try_of(List[str])
def list_of_str_without_explicit_return_type_annotation():
    yield from Success(["still", "well", "typed", ":)"])


@effect.result
def caller():
    yield from (
        a + b + c
        for a in list_of_str()  # pyright infers a: List[str]
        for b in surprise()  # pyright infers b: Any
        for c in list_of_str_without_explicit_return_type_annotation()  # pyright infers c: List[str]
    )

Any thoughts? Is there an easier way to do this already?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.