Package: pyferret
pyferretThis Python library provides functional programming tools like the "maybe" and "result" monads. These monads help handle optional values and error handling in a functional way. The "maybe" monad deals with null or undefined values, while the "result" monad manages computations that may fail and return an error. These tools promote clean, reliable, and concise code by promoting immutability, separation of concerns, and composability.Not pretending on full correspondence with theoretical part of related instruments, because of not all of them can be implemented in Python context with comfortable usage. There may be implemented not all details that required by some abstractions in FP or provided some additional stuff for usability.pyferretInstallationFunction compositionMeaning of ContextValue of FunctorImportance of MonadMaybeHowMaybecan help with function composition?MaybeAPIInitialize instanceisintancechecksUnsafe accessing the valueSafe accessing the valueBoolean checksMapping functionsBinding functionsResultHowResultcan help with function composition?ResultAPIInitialize instanceisintancechecksUnsafe accessing the valueSafe accessing the valueBoolean checksMapping functionsBinding functionsHelpersMaybe from optionalList concatenationTODOInstallationpipinstallpyferretFunction compositionIn Python function composition may be quite nice and useful tool. Function composition is a technique in functional programming where multiple functions are combined together to create a new function. The output of one function becomes the input of the next function, forming a chain of transformations. This allows for the creation of complex and reusable logic by breaking it down into smaller, composable parts.To use a composition that corresponds this description in Python we need some helpful tools which this library provides.We have some calculating functions:defcompute_x()->int|None:...defcompute_y()->int|None:...defcalculate_coef(x:int)->int:...defformat_result(x:str)->str:...defdispatch(x:str)->str|None:...defprocess_x()->str|None:computed_x=compute_x()ifcomputed_x:coef=calculate_coef(computed_x)formatted=format_result(str(coef).upper())returndispatch(formatted)That simple function take too much visual burden. Despite we declared our function with word "process" it's not look like a actual process. We have one simple condition - if some of processing steps isNonethen we returnNone, and every that step requires anifblock to handle.How that can look like with function composition.frompyferret.maybeimportMaybedefcompute_x()->Maybe[int]:...defcompute_y()->Maybe[int]:...defcalculate_coef(x:int)->int:...defformat_result(x:str)->str:...defdispatch(x:str)->Maybe[str]:...# Result type is `Just[str] | Nothing`defprocess_x()->Maybe[str]:return(compute_x().fmap(calculate_coef).fmap(str).fmap(format_result).fmap(str.upper).bind(dispatch))# `process_x` is also can be composed in next calls# `result` type is `Just[int] | Nothing`result=process_x().fmap(str.lower).fmap(str.split).fmap(len)Meaning of ContextIn functional programming, a context refers to additional information or state associated with a computation or value.In here context is simple container that stores a single value, but have meaning for us, like a list or tuple with one element. For example we can have 500 as computation result and 500 as error code, same value and different result.ERROR_CODE=500defcompute_or_error_code()->int:try:_=500/randint(0,1)exceptZeroDivisionError:returnERROR_CODEresult=compute_or_error_code()# In this case we have probability that `result` can be an error and# a computation resultis_error=result==ERROR_CODE# This may be not an errorValue of FunctorFunctor defined here as Context which havefmapmethod.Let's say we have a Functor namedAwith value100(we say that Functor is a Context, while Context is just a container for a value). Thenfmapsimply takes a function that can do operation on type ofAand packs result of function to same Context.result=SomeFunctor(100).fmap(str)# SomeFunctor("100")Importance of MonadMonad defined here as Functor which havebindmethod.Let's say we have a Monad namedAwith value100, and function that takes parameter type ofAand return same MonadBwith different or same type of context value. Sobindtaking this function, provide operation on value ofA, and returnB.op=lambdax:SomeMonad("Yes!")result=SomeMonad(100).bind(op)# SomeMonad("Yes!")MaybeA "maybe" monad handles computations that may or may not produce a value. It represents uncertainty or potential failure. The value can be "Just x" or "Nothing". This allows for chaining computations and concise error handling.Let's dive into example:defcompute_or_none()->int|None:...# Result is the int or None, but None can have meaning in some context,# None is the object which represents void, but it's not a void, it's valueresult=compute_or_none()ERROR_CODE=500defside_effect()->None|Literal[ERROR_CODE]:...result=side_effect()# In that context None represents success of operation, and value represent# an errorifresult==ERROR_CODE:...AMaybemonad can help to qualify what exactly function will return in that case.frompyferret.maybeimportMaybe,Just,Nothingdefcompute_or_none()->Maybe[int]:...result=compute_or_none()# We can check is value present in result by accessing some propertiesifresult.is_some:...ifresult.is_nothing:...ERROR_CODE=500# Same as previous but in manner on `Maybe` usagedefside_effect()->Maybe[ERROR_CODE]:...result=side_effect()ifresult.is_some:...HowMaybecan help with function composition?Maybecan be treated asJust[Any] | Nothing, whereJustandNothingis aContextthat stores a value, butNothingnot providing access to inner value because meaning of this word say that there's can't be a value.We defined that Maybe is a monad, then it havefmap,bindand other helpful methods.MaybeAPIInitialize instance>>>some=Just(1)>>>nothing=Nothing()isintancechecks>>>isinstance(some,Just)True>>>isinstance(nothing,Nothing)TrueUnsafe accessing the value>>>some.value1>>>nothing.valueValueError:AttempttogetvalueonNothingSafe accessing the value>>>some.get_value_or("default")1>>>nothing.get_value_or("default")'default'Boolean checks>>>some.is_someTrue>>>nothing.is_someFalseMapping functionsBasicfmap:>>>some.fmap(lambdax:x*3*10)Just30>>>nothing.fmap(lambdax:x*3*10)NothingWe may need make side effect with value insideJust, but preserve this value and ignore function return:>>>some.fmap_through(lambdax:print(x))1# print(x)Just1# print returns `None`, but some.value is preserved in context>>>nothing.fmap_through(lambdax:print(x))NothingPartial application mapped function:>>>some.fmap_partial(lambdax,y:x*y,y=25)Just25>>>nothing.fmap_partial(lambdax,y:x*y,y=25)NothingPartial application with preserving inner value,fmap_throughandfmap_partialcombined:>>>some.fmap_partial_through(lambdax,y:print(x+y),y=25)26# print(x + y)Just1>>>nothing.fmap_partial_through(lambdax,y:print(x+y),y=25)NothingBinding functionsBasicbind:>>>some.bind(lambdax:Just(x+10))Just11>>>nothing.bind(lambdax:Just(x+10))Nothing>>>some.bind(lambdax:Nothing())Nothing>>>nothing.bind(lambdax:Nothing())NothingBinding and preserving inner value:>>>defside_effect(x:int)->Maybe[str]:...print(x)...returnJust("Ok")...>>>some.bind_through(side_effect)1Just1>>>nothing.bind_through(side_effect)NothingPartial application binding function:>>>some.bind_partial(lambdax,y:Just(x+y),y=34)Just35>>>nothing.bind_partial(lambdax,y:Just(x+y),y=34)NothingPartial application and preserving inner value:>>>defside_effect(x:int,y:int)->Maybe[str]:...print(x+y)...returnJust("Ok")...>>>some.bind_partial_through(side_effect,y=42)43Just1>>>nothing.bind_partial_through(side_effect,y=42)NothingResultThe Result Monad is a way to encapsulate the outcome of a computation in a type that can represent either a successful result or an error.A little example:frompyferret.resultimportResult,Ok,Errdefcompute_or_error()->Result[int,int]:...result=compute_or_error()# We can check success of operation by accessing some propertiesifresult.is_ok:...ifresult.is_err:...ERROR_CODE=500# If side-effect may faildefside_effect()->Result[None,ERROR_CODE]:...result=side_effect()ifresult.is_ok:...HowResultcan help with function composition?Resultcan be treated asOk[Any] | Err[Any], where bothOkandErris aContextthat stores a value, but with semantic about operation result.Result also have all Monad functions -fmap,bind.ResultAPIInitialize instance>>>ok=Ok(1)>>>err=Err("error")isintancechecks>>>isinstance(ok,Ok)True>>>isinstance(err,Err)TrueUnsafe accessing the value>>>ok.ok_value1>>>ok.err_valueValueError:AttempttogeterrvalueonOk:1>>>err.err_value'error'>>>err.ok_valueValueError:AttempttogetokvalueonErr:errorSafe accessing the value>>>ok.get_ok_or("default")1>>>ok.get_err_or("default")'default'>>>err.get_ok_or("default")'default'>>>err.get_err_or("default")'error'Boolean checks>>>ok.is_okTrue>>>ok.is_errFalse>>>err.is_okFalse>>>err.is_errTrueMapping functionsBasicfmap:>>>ok.fmap(lambdax:x*3*10)Ok30>>>err.fmap(lambdax:x*3*10)ErrerrorWe may need make side effect with value insideOk, but preserve this value and ignore function return:>>>ok.fmap_through(lambdax:print(x))1# print(x)Ok1>>>err.fmap_through(lambdax:print(x))Err'error'Partial application mapped function:>>>ok.fmap_partial(lambdax,y:x*y,y=25)Ok25>>>err.fmap_partial(lambdax,y:x*y,y=25)Err'error'Partial application with preserving inner value,fmap_throughandfmap_partialcombined:>>>ok.fmap_partial_through(lambdax,y:print(x+y),y=25)26# print(x + y)Ok1>>>err.fmap_partial_through(lambdax,y:print(x+y),y=25)Err'error'Binding functionsBasicbind:>>>ok.bind(lambdax:Ok(x+10))Ok11>>>err.bind(lambdax:Ok(x+10))Err'error'>>>ok.bind(lambdax:Err("another error"))Err'another error'>>>err.bind(lambdax:Err("another error"))Err'error'Binding and preserving inner value only in case ofOk:>>>defside_effect(x:int)->Result[str,str]:...print(x)...returnOk("Ok")...>>>ok.bind_through(side_effect)1Ok1>>>err.bind_through(side_effect)Err'error'# In that case Err will be taken from `side_effect_error` function>>>defside_effect_error(x:int)->Result[str,str]:...print(x)...returnErr("side effect error")>>>ok.bind_through(side_effect_error)1Err'side effect error'>>>err.bind_through(side_effect_error)Err'error'Partial application binding function:>>>ok.bind_partial(lambdax,y:Ok(x+y),y=34)Ok35>>>ok.bind_partial(lambdax,y:Err(x+y),y=34)Err35>>>err.bind_partial(lambdax,y:Ok(x+y),y=34)Err'error'>>>err.bind_partial(lambdax,y:Err(x+y),y=34)Err'error'Partial application and preserving inner value only if result isOk:>>>defside_effect(x:int,y:int)->Result[str,int]:...print(x+y)...returnOk("Ok")...>>>ok.bind_partial_through(side_effect,y=25)26Ok1>>>err.bind_partial_through(side_effect,y=25)Err'error'>>>ok.bind_partial_through(side_effect_error,y=25)26Err'side effect error'>>>err.bind_partial_through(side_effect_error,y=25)Err'error'HelpersPyfferet provides a set of convenient helper functions to simplify and assist with common tasks.Maybe from optional>>>frompyferret.helpersimportfrom_optional>>>a:int|None=12>>>from_optional(a)Just12>>>a:int|None=None>>>from_optional(a)NothingList concatenation>>>concat([[1,2,3],[4,5,6],[7,8,9]])[1,2,3,4,5,6,7,8,9]TODOMaybemethods that returns value out of contextsResultmethods that returns value out of contextsComplete docsResultmethods for working with exceptions and tracebackResultmethods for fmap and bindErrcontextHelper functionsFrom optional forMaybeConcat list...Maybetest coverageResulttest coverageabstracttest coveragepypipublish and versioningGitGub ActionstypecheckcodestyleHash and compare methodsBetter*argsand**kwargstyping