{"id":19293946,"url":"https://github.com/igrishaev/f","last_synced_at":"2025-05-12T14:50:47.857Z","repository":{"id":57427979,"uuid":"61354848","full_name":"igrishaev/f","owner":"igrishaev","description":"Functional stuff for Python","archived":false,"fork":false,"pushed_at":"2016-07-30T08:33:06.000Z","size":53,"stargazers_count":119,"open_issues_count":1,"forks_count":16,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-01-25T12:31:37.109Z","etag":null,"topics":["clojure","collections","functional-programming","monad","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/igrishaev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-06-17T07:35:05.000Z","updated_at":"2024-05-31T07:46:02.000Z","dependencies_parsed_at":"2022-09-09T08:50:56.309Z","dependency_job_id":null,"html_url":"https://github.com/igrishaev/f","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Ff","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Ff/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Ff/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Ff/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/igrishaev","download_url":"https://codeload.github.com/igrishaev/f/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238059009,"owners_count":19409607,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","collections","functional-programming","monad","python"],"created_at":"2024-11-09T22:36:39.330Z","updated_at":"2025-02-10T05:09:37.346Z","avatar_url":"https://github.com/igrishaev.png","language":"Python","readme":"# `f` is a set of functional tools for Python\n\n## Functions\n\nA bunch of useful functions to work with data structures.\n\n### Protected call (comes from Lua):\n\n```python\nimport f\n\nf.pcall(lambda a, b: a / b, 4, 2)\n\u003e\u003e\u003e (None, 2)\n\nf.pcall(lambda a, b: a / b, 4, 0)\n\u003e\u003e\u003e (ZeroDivisionError('integer division or modulo by zero'), None)\n```\n\nOr use it like a decorator:\n\n```python\n\n@f.pcall_wraps\ndef func(a, b):\n    return a / b\n\nfunc(4, 2)\n\u003e\u003e\u003e (None, 2)\n\nfunc(4, 0)\n\u003e\u003e\u003e (ZeroDivisionError('integer division or modulo by zero'), None)\n\n```\n\n### Attribute and item chain functions handling exceptions:\n\n```python\n# let's say, you have a schema with the following foreign keys:\n# Order --\u003e Office --\u003e Department --\u003e Chief\n\norder = Order.objects.get(id=42)\n\n# OK\nf.achain(model, 'office', 'department', 'chief', 'name')\n\u003e\u003e\u003e John\n\n# Now imagine the `department` field is null-able and has NULL in the DB:\nf.achain(model, 'office', 'department', 'chief', 'name')\n\u003e\u003e\u003e None\n```\n\n```python\ndata = json.loads('{\"result\": [{\"kids\": [{\"age\": 7, \"name\": \"Leo\"}, {\"age\": 1, \"name\": \"Ann\"}], \"name\": \"Ivan\"}, {\"kids\": null, \"name\": \"Juan\"}]}')\n\n# OK\nf.ichain(data, 'result', 0, 'kids', 0, 'age')\n\u003e\u003e\u003e 7\n\n# the chain is broken\nf.ichain(data, 'result', 42, 'kids', 0, 'age')\n\u003e\u003e None\n```\n\n### Threading functions from Clojure\n\nThe first threading macro puts the value into an each form as a first\nargument to a function:\n\n```python\nf.arr1(\n    -42,                        # initial value\n    (lambda a, b: a + b, 2),    # form\n    abs,                        # form\n    str,                        # form\n    (str.replace, \"40\", \"__\")   # form\n)\n\u003e\u003e\u003e \"__\"\n```\n\nThe second threading macro is just the same, but puts a value at the end:\n\n```python\nf.arr2(\n    -2,\n    abs,\n    (lambda a, b: a + b, 2),\n    str,\n    (\"000\".replace, \"0\")\n)\n\u003e\u003e\u003e \"444\"\n```\n\n### Function composition:\n\n```python\ncomp = f.comp(abs, (lambda x: x * 2), str)\ncomp(-42)\n\u003e\u003e\u003e \"84\"\n```\n\n### Every predicate\n\nComposes a super predicate from the passed ones:\n\n```python\npred1 = f.p_gt(0)\npred2 = f.p_even\npred3 = f.p_not_eq(666)\n\nevery = f.every_pred(pred1, pred2, pred3)\n\nresult = filter(every, (-1, 1, -2, 2, 3, 4, 666, -3, 1, 2))\ntuple(result)\n\u003e\u003e\u003e (2, 4, 2)\n```\n\n### Transducer: a quick-and-dirty port from Clojure\n\n```python\nf.transduce(\n    (lambda x: x + 1),\n    (lambda res, item: res + str(item)),\n    (1, 2, 3),\n    \"\"\n)\n\u003e\u003e\u003e \"234\"\n```\n\n### Nth element getters\n\n```python\nf.first((1, 2, 3))\n\u003e\u003e\u003e 1\n\nf.second((1, 2, 3))\n\u003e\u003e\u003e 2\n\nf.third((1, 2, 3))\n\u003e\u003e\u003e 3\n\nf.nth(0, [1, 2, 3])\n\u003e\u003e\u003e 1\n\nf.nth(9, [1, 2, 3])\n\u003e\u003e\u003e None\n```\n\n## Predicates\n\nA set of unary and binary predicates.\n\nUnary example:\n\n```python\nf.p_str(\"test\")\n\u003e\u003e\u003e True\n\nf.p_str(0)\n\u003e\u003e\u003e False\n\nf.p_str(u\"test\")\n\u003e\u003e\u003e True\n\n# checks for both int and float types\nf.p_num(1), f.p_num(1.0)\n\u003e\u003e\u003e True, True\n\nf.p_list([])\n\u003e\u003e\u003e True\n\nf.p_truth(1)\n\u003e\u003e\u003e True\n\nf.p_truth(None)\n\u003e\u003e\u003e False\n\nf.p_none(None)\n\u003e\u003e\u003e True\n```\n\nBinary example:\n\n```python\np = f.p_gt(0)\n\np(1), p(100), p(0), p(-1)\n\u003e\u003e\u003e True, True, False, False\n\np = f.p_gte(0)\np(0), p(1), p(-1)\n\u003e\u003e\u003e True, True, False\n\np = f.p_eq(42)\np(42), p(False)\n\u003e\u003e\u003e True, False\n\nob1 = object()\np = f.p_is(ob1)\np(object())\n\u003e\u003e\u003e False\np(ob1)\n\u003e\u003e\u003e True\n\np = f.p_in((1, 2, 3))\np(1), p(3)\n\u003e\u003e\u003e True, True\np(4)\n\u003e\u003e\u003e False\n```\n\nYou may combine predicates with `f.comp` or `f.every_pred`:\n\n```python\n# checks for positive even number\npred = f.every_pred(f.p_num, f.p_even, f.p_gt(0))\npred(None), pred(-1), pred(5)\n\u003e\u003e\u003e False, False, False\npred(6)\n\u003e\u003e\u003e True\n```\n\n## Collections\n\nImproved collections `List`, `Tuple`, `Dict` and `Set` with the following\nfeatures.\n\n### Square braces syntax for initiating\n\n```python\nf.List[1, 2, 3]     # or just f.L\n\u003e\u003e\u003e List[1, 2, 3]\n\nf.T[1, 2, 3]\n\u003e\u003e\u003e Tuple(1, 2, 3)\n\nf.Set[1, 2, 3]\n\u003e\u003e\u003e Set{1, 2, 3}\n\nf.D[1: 2, 2: 3]\n\u003e\u003e\u003e Dict{1: 2, 2: 3}\n```\n\n### Additional methods such as .map, .filter, .foreach, .sum, etc:\n\n```python\nl1 = f.L[1, 2, 3]\nl1.map(str).join(\"-\")\n\u003e\u003e\u003e \"1-2-3\"\n\nresult = []\n\ndef collect(x, delta=0):\n    result.append(x + delta)\n\nl1.foreach(collect, delta=1)\nresult == [2, 3, 4]\n\u003e\u003e\u003e True\n```\n\nSee the source code for more methods.\n\n### Every method returns a new collection of this type:\n\n```python\nl1.filter(f.p_even)\n\u003e\u003e\u003e List[2]\n\nl1.group(2)\n\u003e\u003e\u003e List[List[1, 2], List[3]]\n\n# filtering a dict:\nf.D[1: 1, 2: 2, 0: 2].filter(lambda (k, v): k + v == 2)\n\u003e\u003e\u003e Dict{0: 2, 1: 1}\n```\n\n### Easy adding two collection of different types\n\n```python\n\n# merging dicts\nf.D(a=1, b=2, c=3) + {\"d\": 4, \"e\": 5, \"f\": 5}\n\u003e\u003e\u003e Dict{'a': 1, 'c': 3, 'b': 2, 'e': 5, 'd': 4, 'f': 5}\n\nf.S[1, 2, 3] + [\"a\", 1, \"b\", 3, \"c\"]\n\u003e\u003e\u003e Set{'a', 1, 2, 3, 'c', 'b'}\n\n# adding list with tuple\nf.L[1, 2, 3] + (4, )\nList[1, 2, 3, 4]\n```\n\n### Quick turning to another collection\n\n```python\nf.L[\"a\", 1, \"b\", 2].group(2).D()\n\u003e\u003e\u003e Dict{\"a\": 1, \"b\": 2}\n\nf.L[1, 2, 3, 3, 2, 1].S().T()\n\u003e\u003e\u003e Tuple[1, 2, 3]\n```\n\n## Monads\n\nThere are Maybe, Either, Error and IO monads are in the library. Most of them\nare based on classical Haskell definitions. The main difference is they use\npredicates instead of type checks.\n\nI had to implement `\u003e\u003e=` operator as `\u003e\u003e` (right binary shift). There is also a\nPython-specific `.get()` method to fetch an actual value from a monadic\ninstance. Be fair and use it only at the end of the monadic computation!\n\n### Maybe\n\n```python\n# Define a monadic constructor\nMaybeInt = f.maybe(f.p_int)\n\nMaybeInt(2)\n\u003e\u003e\u003e Just[2]\n\nMaybeInt(\"not an int\")\n\u003e\u003e\u003e Nothing\n\n# Monadic pipeline\nMaybeInt(2) \u003e\u003e (lambda x: MaybeInt(x + 2))\n\u003e\u003e\u003e Just[4]\n\n# Nothing breaks the pipeline\nMaybeInt(2) \u003e\u003e (lambda x: f.Nothing()) \u003e\u003e (lambda x: MaybeInt(x + 2))\n\u003e\u003e\u003e Nothing\n```\n\nThe better way to engage monads into you project is to use monadic decorators:\n\n```python\n@f.maybe_wraps(f.p_num)\ndef mdiv(a, b):\n    if b:\n        return a / b\n    else:\n        return None\n\nmdiv(4, 2)\n\u003e\u003e\u003e Just[2]\n\nmdiv(4, 0)\n\u003e\u003e\u003e Nothing\n```\n\nUse `.bind` method as an alias to `\u003e\u003e`:\n\n```python\n\nMaybeInt(2).bind(lambda x: MaybeInt(x + 1))\n\u003e\u003e\u003e Just[3]\n```\n\nYou may pass additional arguments to both `.bind` and `\u003e\u003e` methods:\n\n```python\nMaybeInt(6) \u003e\u003e (mdiv, 2)\n\u003e\u003e\u003e Just[3]\n\nMaybeInt(6).bind(mdiv, 2)\n\u003e\u003e\u003e Just[3]\n```\n\nRelease the final value:\n\n```python\nm = MaybeInt(2) \u003e\u003e (lambda x: MaybeInt(x + 2))\nm.get()\n\u003e\u003e\u003e 3\n```\n\n### Either\n\nThis monad presents two possible values: Left (negative) and Right\n(positive).\n\n```python\n# create a constructor based on left and right predicates.\nEitherStrNum = f.either(f.p_str, f.p_num)\n\nEitherStrNum(\"error\")\n\u003e\u003e\u003e Left[error]\n\nEitherStrNum(42)\n\u003e\u003e\u003e Right[42]\n```\n\nRight value follows the pipeline, but Left breaks it.\n\n```python\nEitherStrNum(1) \u003e\u003e (lambda x: EitherStrNum(x + 1))\n\u003e\u003e\u003e Right[2]\n\nEitherStrNum(1) \u003e\u003e (lambda x: EitherStrNum(\"error\")) \u003e\u003e (lambda x: EitherStrNum(x + 1))\n\u003e\u003e\u003e Left[error]\n```\n\nWhen the plain value does not fit both predicates, `TypeError` occurs:\n\n```python\nEitherStrNum(None)\n\u003e\u003e\u003e TypeError: Value None doesn't fit...\n```\n\nUse decorator to wrap an existing function with Either logic:\n\n```python\n@f.either_wraps(f.p_str, f.p_num)\ndef ediv(a, b):\n    if b == 0:\n        return \"Div by zero: %s / %s\" % (a, b)\n    else:\n        return a / b\n\n\n@f.either_wraps(f.p_str, f.p_num)\ndef esqrt(a):\n    if a \u003c 0:\n        return \"Negative number: %s\" % a\n    else:\n        return math.sqrt(a)\n\n\nEitherStrNum(16) \u003e\u003e (ediv, 4) \u003e\u003e esqrt\n\u003e\u003e\u003e Right[2.0]\n\nEitherStrNum(16) \u003e\u003e (ediv, 0) \u003e\u003e esqrt\n\u003e\u003e\u003e Left[Div by zero: 16 / 0]\n```\n\n### IO\n\nThis monad wraps a function that does I/O operations. All the further calls\nreturn monadic instances of the result.\n\n```\nIoPrompt = f.io(lambda prompt: raw_input(prompt))\nIoPrompt(\"Your name: \")  # prompts for you name, I'll type \"Ivan\" and RET\n\u003e\u003e\u003e IO[Ivan]\n```\n\nOr use decorator:\n\n```python\nimport sys\n\n@f.io_wraps\ndef input(msg):\n    return raw_input(msg)\n\n@f.io_wraps\ndef write(text, chan):\n    chan.write(text)\n\ninput(\"name: \") \u003e\u003e (write, sys.stdout)\n\u003e\u003e\u003e name: Ivan\n\u003e\u003e\u003e Ivan\n\u003e\u003e\u003e IO[None]\n```\n\n### Error\n\nError monad also known as `Try` in Scala is to prevent rising\nexceptions. Instead, it provides `Success` sub-class to wrap positive result and\n`Failture` to wrap an occured exception.\n\n```\nError = f.error(lambda a, b: a / b)\n\nError(4, 2)\n\u003e\u003e\u003e Success[2]\n\nError(4, 0)\n\u003e\u003e\u003e Failture[integer division or modulo by zero]\n```\n\nGetting a value from `Failture` with `.get` method will re-rise it. Use\n`.recover` method to deal with exception in a safe way.\n\n```python\nError(4, 0).get()\nZeroDivisionError: integer division or modulo by zero\n\n# value variant\nError(4, 0).recover(ZeroDivisionError, 42)\nSuccess[2]\n```\n\nYou may pass a tuple of exception classes. A value might be a function that\ntakes a exception instance and returns a proper value:\n\n```python\n\ndef handler(e):\n    logger.exception(e)\n    return 0\n\nError(4, 0).recover((ZeroDivisionError, TypeError), handler)\n\u003e\u003e\u003e Success[0]\n```\n\nDecorator variant:\n\n```python\n@f.error_wraps\ndef tdiv(a, b):\n    return a / b\n\n\n@f.error_wraps\ndef tsqrt(a):\n    return math.sqrt(a)\n\ntdiv(16, 4) \u003e\u003e tsqrt\n\u003e\u003e\u003e Success[2.0]\n\ntsqrt(16).bind(tdiv, 2)\n\u003e\u003e\u003e Success[2.0]\n```\n\n## Generics\n\nGeneric is a flexible callable object that may have different strategies\ndepending on a set of predicates (guards).\n\n```python\n# Create an instance\ngen = f.Generic()\n\n# extend it with handlers\n@gen.extend(f.p_int, f.p_str)\ndef handler1(x, y):\n    return str(x) + y\n\n@gen.extend(f.p_int, f.p_int)\ndef handler2(x, y):\n    return x + y\n\n@gen.extend(f.p_str, f.p_str)\ndef handler3(x, y):\n    return x + y + x + y\n\n@gen.extend(f.p_str)\ndef handler4(x):\n    return \"-\".join(reversed(x))\n\n@gen.extend()\ndef handler5():\n    return 42\n\n@gen.extend(f.p_none)\ndef handler6(x):\n    return gen(1, 2)\n\n# let's try:\ngen(None)\n\u003e\u003e\u003e 3\n\ngen(1, \"2\")\n\u003e\u003e\u003e \"12\"\n\ngen(1, 2)\n\u003e\u003e\u003e 3\n\ngen(\"fiz\", \"baz\")\n\u003e\u003e\u003e \"fizbazfizbaz\"\n\ngen(\"hello\")\n\u003e\u003e\u003e \"o-l-l-e-h\"\n\ngen()\n\u003e\u003e\u003e 42\n\n# calling without a default handler\ngen(1, 2, 3, 4)\n\u003e\u003e\u003e TypeError exception goes here...\n\n# now we have one\n@gen.default\ndef default_handler(*args):\n    return \"default\"\n\ngen(1, 2, 3, 4)\n\u003e\u003e\u003e \"default\"\n```\n","funding_links":[],"categories":["Libraries"],"sub_categories":["[Python](https://www.python.org/)"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figrishaev%2Ff","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Figrishaev%2Ff","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figrishaev%2Ff/lists"}