{"id":20977236,"url":"https://github.com/technologicat/unpythonic","last_synced_at":"2025-04-08T04:16:34.574Z","repository":{"id":62586484,"uuid":"142169961","full_name":"Technologicat/unpythonic","owner":"Technologicat","description":"Supercharge your Python with parts of Lisp and Haskell.","archived":false,"fork":false,"pushed_at":"2024-09-27T14:56:40.000Z","size":5866,"stargazers_count":93,"open_issues_count":25,"forks_count":3,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-28T18:07:17.724Z","etag":null,"topics":["continuations","cpython","currying","dynamic-variable","functional-programming","haskell","language-extension","lazy-evaluation","lisp","macros","mcpyrate","multiple-dispatch","pypy3","python","python3","racket","scheme","syntactic-macros","tail-call-optimization","tco"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Technologicat.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":"AUTHORS.md","dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-07-24T14:27:07.000Z","updated_at":"2025-02-09T22:50:29.000Z","dependencies_parsed_at":"2024-11-19T22:37:26.338Z","dependency_job_id":null,"html_url":"https://github.com/Technologicat/unpythonic","commit_stats":{"total_commits":3228,"total_committers":4,"mean_commits":807.0,"dds":"0.44206939281288726","last_synced_commit":"4f85957bf64e1b786da0679eade3fe602793ceee"},"previous_names":[],"tags_count":42,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Technologicat%2Funpythonic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Technologicat%2Funpythonic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Technologicat%2Funpythonic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Technologicat%2Funpythonic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Technologicat","download_url":"https://codeload.github.com/Technologicat/unpythonic/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247773720,"owners_count":20993639,"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":["continuations","cpython","currying","dynamic-variable","functional-programming","haskell","language-extension","lazy-evaluation","lisp","macros","mcpyrate","multiple-dispatch","pypy3","python","python3","racket","scheme","syntactic-macros","tail-call-optimization","tco"],"created_at":"2024-11-19T04:57:42.556Z","updated_at":"2025-04-08T04:16:34.549Z","avatar_url":"https://github.com/Technologicat.png","language":"Python","readme":"# Unpythonic: Python meets Lisp and Haskell\n\nIn the spirit of [toolz](https://github.com/pytoolz/toolz), we provide missing features for Python, mainly from the list processing tradition, but with some Haskellisms mixed in. We extend the language with a set of [syntactic macros](https://en.wikipedia.org/wiki/Macro_(computer_science)#Syntactic_macros). We also provide an in-process, background [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) server for live inspection and hot-patching. The emphasis is on **clear, pythonic syntax**, **making features work together**, and **obsessive correctness**.\n\n![100% Python](https://img.shields.io/github/languages/top/Technologicat/unpythonic) ![supported language versions](https://img.shields.io/pypi/pyversions/unpythonic) ![supported implementations](https://img.shields.io/pypi/implementation/unpythonic) ![CI status](https://img.shields.io/github/actions/workflow/status/Technologicat/unpythonic/python-package.yml?branch=master) [![codecov](https://codecov.io/gh/Technologicat/unpythonic/branch/master/graph/badge.svg)](https://codecov.io/gh/Technologicat/unpythonic)  \n![version on PyPI](https://img.shields.io/pypi/v/unpythonic) ![PyPI package format](https://img.shields.io/pypi/format/unpythonic) ![dependency status](https://img.shields.io/librariesio/github/Technologicat/unpythonic)  \n![license: BSD](https://img.shields.io/pypi/l/unpythonic) ![open issues](https://img.shields.io/github/issues/Technologicat/unpythonic) [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](http://makeapullrequest.com/)\n\n*Some hypertext features of this README, such as local links to detailed documentation, and expandable example highlights, are not supported when viewed on PyPI; [view on GitHub](https://github.com/Technologicat/unpythonic) to have those work properly.*\n\n\n### Dependencies\n\nNone required.\n\n - [`mcpyrate`](https://github.com/Technologicat/mcpyrate) optional, to enable the syntactic macro layer, an interactive macro REPL, and some example dialects.\n\nAs of v0.15.3, `unpythonic` runs on CPython 3.8, 3.9 and 3.10, 3.11, 3.12, and PyPy3 (language versions 3.8, 3.9, 3.10); the [CI](https://en.wikipedia.org/wiki/Continuous_integration) process verifies the tests pass on those platforms. New Python versions are added and old ones are removed following the [Long-term support roadmap](https://github.com/Technologicat/unpythonic/issues/1).\n\n\n### Documentation\n\n- **README**: you are here.\n- [Pure-Python feature set](doc/features.md)\n- [Syntactic macro feature set](doc/macros.md)\n- [Examples of creating dialects using `mcpyrate`](doc/dialects.md): Python the way you want it.\n- [REPL server](doc/repl.md): interactively hot-patch your running Python program.\n- [Troubleshooting](doc/troubleshooting.md): possible solutions to possibly common issues.\n- [Design notes](doc/design-notes.md): for more insight into the design choices of ``unpythonic``.\n- [Essays](doc/essays.md): for writings on the philosophy of ``unpythonic``, things that inspired it, and related discoveries.\n- [Additional reading](doc/readings.md): links to material relevant in the context of ``unpythonic``.\n- [Contribution guidelines](CONTRIBUTING.md): for understanding the codebase, or if you're interested in making a code or documentation PR.\n\nThe features of `unpythonic` are built out of, in increasing order of [magic](https://macropy3.readthedocs.io/en/latest/discussion.html#levels-of-magic):\n\n - Pure Python (e.g. batteries for `itertools`),\n - Macros driving a pure-Python core (`do`, `let`),\n - Pure macros (e.g. `continuations`, `lazify`, `dbg`).\n - Whole-module transformations, a.k.a. dialects (e.g. `Lispy`).\n\nThis depends on the purpose of each feature, as well as ease-of-use considerations. See the design notes for more information.\n\n\n### Examples\n\nSmall, limited-space overview of the overall flavor. There is a lot more that does not fit here, especially in the pure-Python feature set. We give here simple examples that are **not** necessarily of the most general form supported by the constructs. See the [full documentation](doc/features.md) and [unit tests](unpythonic/tests/) for more examples.\n\n#### Unpythonic in 30 seconds: Pure Python\n\n\u003cdetails\u003e\u003csummary\u003eLoop functionally, with tail call optimization.\u003c/summary\u003e\n\n[[docs](doc/features.md#looped-looped_over-loops-in-fp-style-with-tco)]\n\n```python\nfrom unpythonic import looped, looped_over\n\n@looped\ndef result(loop, acc=0, i=0):\n    if i == 10:\n        return acc\n    else:\n        return loop(acc + i, i + 1)  # tail call optimized, no call stack blowup.\nassert result == 45\n\n@looped_over(range(3), acc=[])\ndef result(loop, i, acc):\n    acc.append(lambda x: i * x)  # fresh \"i\" each time, no mutation of loop counter.\n    return loop()\nassert [f(10) for f in result] == [0, 10, 20]\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eIntroduce dynamic variables.\u003c/summary\u003e\n\n[[docs](doc/features.md#dyn-dynamic-assignment)]\n\n```python\nfrom unpythonic import dyn, make_dynvar\n\nmake_dynvar(x=42)  # set a default value\n\ndef f():\n    assert dyn.x == 17\n    with dyn.let(x=23):\n        assert dyn.x == 23\n        g()\n    assert dyn.x == 17\n\ndef g():\n    assert dyn.x == 23\n\nassert dyn.x == 42\nwith dyn.let(x=17):\n    assert dyn.x == 17\n    f()\nassert dyn.x == 42\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eInteractively hot-patch your running Python program.\u003c/summary\u003e\n\n[[docs](doc/repl.md)]\n\nTo opt in, add just two lines of code to your main program:\n\n```python\nfrom unpythonic.net import server\nserver.start(locals={})  # automatically daemonic\n\nimport time\n\ndef main():\n    while True:\n        time.sleep(1)\n\nif __name__ == '__main__':\n    main()\n```\n\nOr if you just want to take this for a test run, start the built-in demo app:\n\n```bash\npython3 -m unpythonic.net.server\n```\n\nOnce a server is running, to connect:\n\n```bash\npython3 -m unpythonic.net.client 127.0.0.1\n```\n\nThis gives you a REPL, inside your live process, with all the power of Python. You can `importlib.reload` any module, and through `sys.modules`, inspect or overwrite any name at the top level of any module. You can `pickle.dump` your data. Or do anything you want with/to the live state of your app.\n\nYou can have multiple REPL sessions connected simultaneously. When your app exits (for any reason), the server automatically shuts down, closing all connections if any remain. But exiting the client leaves the server running, so you can connect again later - that's the whole point.\n\nOptionally, if you have [mcpyrate](https://github.com/Technologicat/mcpyrate), the REPL sessions support importing, invoking and defining macros.\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eIndustrial-strength scan and fold.\u003c/summary\u003e\n\n[[docs](doc/features.md#batteries-for-itertools)]\n\nScan and fold accept multiple iterables, like in Racket.\n\n```python\nfrom operator import add\nfrom unpythonic import scanl, foldl, unfold, take, Values\n\nassert tuple(scanl(add, 0, range(1, 5))) == (0, 1, 3, 6, 10)\n\ndef op(e1, e2, acc):\n    return acc + e1 * e2\nassert foldl(op, 0, (1, 2), (3, 4)) == 11\n\ndef nextfibo(a, b):\n    return Values(a, a=b, b=a + b)\nassert tuple(take(10, unfold(nextfibo, 1, 1))) == (1, 1, 2, 3, 5, 8, 13, 21, 34, 55)\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eIndustrial-strength curry.\u003c/summary\u003e\n\n[[docs](doc/features.md#batteries-for-functools)]\n\nWe bind arguments to parameters like Python itself does, so it does not matter whether arguments are passed by position or by name during currying. We support `@generic` multiple-dispatch functions.\n\nWe also feature a Haskell-inspired passthrough system: any args and kwargs that are not accepted by the call signature will be passed through. This is useful when a curried function returns a new function, which is then the target for the passthrough. See the docs for details.\n\n```python\nfrom unpythonic import curry, generic, foldr, composerc, cons, nil, ll\n\n@curry\ndef f(x, y):\n    return x, y\n\nassert f(1, 2) == (1, 2)\nassert f(1)(2) == (1, 2)\nassert f(1)(y=2) == (1, 2)\nassert f(y=2)(x=1) == (1, 2)\n\n@curry\ndef add3(x, y, z):\n    return x + y + z\n\n# actually uses partial application so these work, too\nassert add3(1)(2)(3) == 6\nassert add3(1, 2)(3) == 6\nassert add3(1)(2, 3) == 6\nassert add3(1, 2, 3) == 6\n\n@curry\ndef lispyadd(*args):\n    return sum(args)\nassert lispyadd() == 0  # no args is a valid arity here\n\n@generic\ndef g(x: int, y: int):\n    return \"int\"\n@generic\ndef g(x: float, y: float):\n    return \"float\"\n@generic\ndef g(s: str):\n    return \"str\"\ng = curry(g)\n\nassert callable(g(1))\nassert g(1)(2) == \"int\"\n\nassert callable(g(1.0))\nassert g(1.0)(2.0) == \"float\"\n\nassert g(\"cat\") == \"str\"\nassert g(s=\"cat\") == \"str\"\n\n# simple example of passthrough\nmymap = lambda f: curry(foldr, composerc(cons, f), nil)\nmyadd = lambda a, b: a + b\nassert curry(mymap, myadd, ll(1, 2, 3), ll(2, 4, 6)) == ll(3, 6, 9)\n```\n\u003c/details\u003e\n\u003cdetails\u003e\u003csummary\u003eMultiple-dispatch generic functions, like in CLOS or Julia.\u003c/summary\u003e\n\n[[docs](doc/features.md#generic-typed-isoftype-multiple-dispatch)]\n\n```python\nfrom unpythonic import generic\n\n@generic\ndef my_range(stop: int):  # create the generic function and the first multimethod\n    return my_range(0, 1, stop)\n@generic\ndef my_range(start: int, stop: int):  # further registrations add more multimethods\n    return my_range(start, 1, stop)\n@generic\ndef my_range(start: int, step: int, stop: int):\n    return start, step, stop\n```\n\nThis is a purely run-time implementation, so it does **not** give performance benefits, but it can make code more readable, and makes it modular to add support for new input types (or different call signatures) to an existing function later.\n\n[*Holy traits*](https://ahsmart.com/pub/holy-traits-design-patterns-and-best-practice-book/) are also a possibility:\n\n```python\nimport typing\nfrom unpythonic import generic, augment\n\nclass FunninessTrait:\n    pass\nclass IsFunny(FunninessTrait):\n    pass\nclass IsNotFunny(FunninessTrait):\n    pass\n\n@generic\ndef funny(x: typing.Any):  # default\n    raise NotImplementedError(f\"`funny` trait not registered for anything matching {type(x)}\")\n\n@augment(funny)\ndef funny(x: str):  # noqa: F811\n    return IsFunny()\n@augment(funny)\ndef funny(x: int):  # noqa: F811\n    return IsNotFunny()\n\n@generic\ndef laugh(x: typing.Any):\n    return laugh(funny(x), x)\n\n@augment(laugh)\ndef laugh(traitvalue: IsFunny, x: typing.Any):\n    return f\"Ha ha ha, {x} is funny!\"\n@augment(laugh)\ndef laugh(traitvalue: IsNotFunny, x: typing.Any):\n    return f\"{x} is not funny.\"\n\nassert laugh(\"that\") == \"Ha ha ha, that is funny!\"\nassert laugh(42) == \"42 is not funny.\"\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eConditions: resumable, modular error handling, like in Common Lisp.\u003c/summary\u003e\n\n[[docs](doc/features.md#handlers-restarts-conditions-and-restarts)]\n\nContrived example:\n\n```python\nfrom unpythonic import error, restarts, handlers, invoke, use_value, unbox\n\nclass MyError(ValueError):\n    def __init__(self, value):  # We want to act on the value, so save it.\n        self.value = value\n\ndef lowlevel(lst):\n    _drop = object()  # gensym/nonce\n    out = []\n    for k in lst:\n        # Provide several different error recovery strategies.\n        with restarts(use_value=(lambda x: x),\n                      halve=(lambda x: x // 2),\n                      drop=(lambda: _drop)) as result:\n            if k \u003e 9000:\n                error(MyError(k))\n            # This is reached when no error occurs.\n            # `result` is a box, send k into it.\n            result \u003c\u003c k\n        # Now the result box contains either k,\n        # or the return value of one of the restarts.\n        r = unbox(result)  # get the value from the box\n        if r is not _drop:\n            out.append(r)\n    return out\n\ndef highlevel():\n    # Choose which error recovery strategy to use...\n    with handlers((MyError, lambda c: use_value(c.value))):\n        assert lowlevel([17, 10000, 23, 42]) == [17, 10000, 23, 42]\n\n    # ...on a per-use-site basis...\n    with handlers((MyError, lambda c: invoke(\"halve\", c.value))):\n        assert lowlevel([17, 10000, 23, 42]) == [17, 5000, 23, 42]\n\n    # ...without changing the low-level code.\n    with handlers((MyError, lambda: invoke(\"drop\"))):\n        assert lowlevel([17, 10000, 23, 42]) == [17, 23, 42]\n\nhighlevel()\n```\n\nConditions only shine in larger systems, with restarts set up at multiple levels of the call stack; this example is too small to demonstrate that. The single-level case here could be implemented as a error-handling mode parameter for the example's only low-level function.\n\nWith multiple levels, it becomes apparent that this mode parameter must be threaded through the API at each level, unless it is stored as a dynamic variable (see [`unpythonic.dyn`](doc/features.md#dyn-dynamic-assignment)). But then, there can be several types of errors, and the error-handling mode parameters - one for each error type - have to be shepherded in an intricate manner. A stack is needed, so that an inner level may temporarily override the handler for a particular error type...\n\nThe condition system is the clean, general solution to this problem. It automatically scopes handlers to their dynamic extent, and manages the handler stack automatically. In other words, it dynamically binds error-handling modes (for several types of errors, if desired) in a controlled, easily understood manner. The local programmability (i.e. the fact that a handler is not just a restart name, but an arbitrary function) is a bonus for additional flexibility.\n\nIf this sounds a lot like an exception system, that's because conditions are the supercharged sister of exceptions. The condition model cleanly separates mechanism from policy, while otherwise remaining similar to the exception model.\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eLispy symbol type.\u003c/summary\u003e\n\n[[docs](doc/features.md#sym-gensym-Singleton-symbols-and-singletons)]\n\nRoughly, a [symbol](https://stackoverflow.com/questions/8846628/what-exactly-is-a-symbol-in-lisp-scheme) is a guaranteed-[interned](https://en.wikipedia.org/wiki/String_interning) string.\n\nA [gensym](http://clhs.lisp.se/Body/f_gensym.htm) is a guaranteed-*unique* string, which is useful as a nonce value. It's similar to the pythonic idiom `nonce = object()`, but with a nice repr, and object-identity-preserving pickle support.\n\n```python\nfrom unpythonic import sym  # lispy symbol\nsandwich = sym(\"sandwich\")\nhamburger = sym(\"sandwich\")  # symbol's identity is determined by its name, only\nassert hamburger is sandwich\n\nassert str(sandwich) == \"sandwich\"  # symbols have a nice str()\nassert repr(sandwich) == 'sym(\"sandwich\")'  # and eval-able repr()\nassert eval(repr(sandwich)) is sandwich\n\nfrom pickle import dumps, loads\npickled_sandwich = dumps(sandwich)\nunpickled_sandwich = loads(pickled_sandwich)\nassert unpickled_sandwich is sandwich  # symbols survive a pickle roundtrip\n\nfrom unpythonic import gensym  # gensym: make new uninterned symbol\ntabby = gensym(\"cat\")\nscottishfold = gensym(\"cat\")\nassert tabby is not scottishfold\n\npickled_tabby = dumps(tabby)\nunpickled_tabby = loads(pickled_tabby)\nassert unpickled_tabby is tabby  # also gensyms survive a pickle roundtrip\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eLispy data structures.\u003c/summary\u003e\n\n[[docs for `box`](doc/features.md#box-a-mutable-single-item-container)] [[docs for `cons`](doc/features.md#cons-and-friends-pythonic-lispy-linked-lists)] [[docs for `frozendict`](doc/features.md#frozendict-an-immutable-dictionary)]\n\n```python\nfrom unpythonic import box, unbox  # mutable single-item container\ncat = object()\ncardboardbox = box(cat)\nassert cardboardbox is not cat  # the box is not the cat\nassert unbox(cardboardbox) is cat  # but the cat is inside the box\nassert cat in cardboardbox  # ...also syntactically\ndog = object()\ncardboardbox \u003c\u003c dog  # hey, it's my box! (replace contents)\nassert unbox(cardboardbox) is dog\n\nfrom unpythonic import cons, nil, ll, llist  # lispy linked lists\nlst = cons(1, cons(2, cons(3, nil)))\nassert ll(1, 2, 3) == lst  # make linked list out of elements\nassert llist([1, 2, 3]) == lst  # convert iterable to linked list\n\nfrom unpythonic import frozendict  # immutable dictionary\nd1 = frozendict({'a': 1, 'b': 2})\nd2 = frozendict(d1, c=3, a=4)\nassert d1 == frozendict({'a': 1, 'b': 2})\nassert d2 == frozendict({'a': 4, 'b': 2, 'c': 3})\n```\n\u003c/details\u003e\n\u003cdetails\u003e\u003csummary\u003eAllow a lambda to call itself. Name a lambda.\u003c/summary\u003e\n\n[[docs for `withself`](doc/features.md#batteries-for-functools)] [[docs for `namelambda`](doc/features.md#namelambda-rename-a-function)]\n\n```python\nfrom unpythonic import withself, namelambda\n\nfact = withself(lambda self, n: n * self(n - 1) if n \u003e 1 else 1)  # see @trampolined to do this with TCO\nassert fact(5) == 120\n\nsquare = namelambda(\"square\")(lambda x: x**2)\nassert square.__name__ == \"square\"\nassert square.__qualname__ == \"square\"  # or e.g. \"somefunc.\u003clocals\u003e.square\" if inside a function\nassert square.__code__.co_name == \"square\"  # used by stack traces\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eBreak infinite recursion cycles.\u003c/summary\u003e\n\n[[docs](doc/features.md#fix-break-infinite-recursion-cycles)]\n\n```python\nfrom typing import NoReturn\nfrom unpythonic import fix\n\n@fix()\ndef a(k):\n    return b((k + 1) % 3)\n@fix()\ndef b(k):\n    return a((k + 1) % 3)\nassert a(0) is NoReturn\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eBuild number sequences by example. Slice general iterables.\u003c/summary\u003e\n\n[[docs for `s`](doc/features.md#s-m-mg-lazy-mathematical-sequences-with-infix-arithmetic)] [[docs for `islice`](doc/features.md#islice-slice-syntax-support-for-itertoolsislice)]\n\n```python\nfrom unpythonic import s, islice\n\nseq = s(1, 2, 4, ...)\nassert tuple(islice(seq)[:10]) == (1, 2, 4, 8, 16, 32, 64, 128, 256, 512)\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eMemoize functions and generators.\u003c/summary\u003e\n\n[[docs for `memoize`](doc/features.md#batteries-for-functools)] [[docs for `gmemoize`](doc/features.md#gmemoize-imemoize-fimemoize-memoize-generators)]\n\n```python\nfrom itertools import count, takewhile\nfrom unpythonic import memoize, gmemoize, islice\n\nncalls = 0\n@memoize  # \u003c-- important part\ndef square(x):\n    global ncalls\n    ncalls += 1\n    return x**2\nassert square(2) == 4\nassert ncalls == 1\nassert square(3) == 9\nassert ncalls == 2\nassert square(3) == 9\nassert ncalls == 2  # called only once for each unique set of arguments\n\n# \"memoize lambda\": classic evaluate-at-most-once thunk\nthunk = memoize(lambda: print(\"hi from thunk\"))\nthunk()  # the message is printed only the first time\nthunk()\n\n@gmemoize  # \u003c-- important part\ndef primes():  # FP sieve of Eratosthenes\n    yield 2\n    for n in count(start=3, step=2):\n        if not any(n % p == 0 for p in takewhile(lambda x: x*x \u003c= n, primes())):\n            yield n\n\nassert tuple(islice(primes())[:10]) == (2, 3, 5, 7, 11, 13, 17, 19, 23, 29)\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eFunctional updates.\u003c/summary\u003e\n\n[[docs](doc/features.md#fup-functional-update-shadowedsequence)]\n\n```python\nfrom itertools import repeat\nfrom unpythonic import fup\n\nt = (1, 2, 3, 4, 5)\ns = fup(t)[0::2] \u003c\u003c repeat(10)\nassert s == (10, 2, 10, 4, 10)\nassert t == (1, 2, 3, 4, 5)\n\nfrom itertools import count\nfrom unpythonic import imemoize\nt = (1, 2, 3, 4, 5)\ns = fup(t)[::-2] \u003c\u003c imemoize(count(start=10))()\nassert s == (12, 2, 11, 4, 10)\nassert t == (1, 2, 3, 4, 5)\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eLive list slices.\u003c/summary\u003e\n\n[[docs](doc/features.md#view-writable-sliceable-view-into-a-sequence)]\n\n```python\nfrom unpythonic import view\n\nlst = list(range(10))\nv = view(lst)[::2]  # [0, 2, 4, 6, 8]\nv[2:4] = (10, 20)  # re-slicable, still live.\nassert lst == [0, 1, 2, 3, 10, 5, 20, 7, 8, 9]\n\nlst[2] = 42\nassert v == [0, 42, 10, 20, 8]\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003ePipes: method chaining syntax for regular functions.\u003c/summary\u003e\n\n[[docs](doc/features.md#pipe-piped-lazy_piped-sequence-functions)]\n\n```python\nfrom unpythonic import piped, exitpipe\n\ndouble = lambda x: 2 * x\ninc    = lambda x: x + 1\nx = piped(42) | double | inc | exitpipe\nassert x == 85\n```\n\nThe point is usability: in a function composition using pipe syntax, data flows from left to right.\n\u003c/details\u003e\n\n\n#### Unpythonic in 30 seconds: Language extensions with macros\n\n\u003cdetails\u003e\u003csummary\u003eunpythonic.test.fixtures: a minimalistic test framework for macro-enabled Python.\u003c/summary\u003e\n\n[[docs](doc/macros.md#unpythonictestfixtures-a-test-framework-for-macro-enabled-python)]\n\n```python\nfrom unpythonic.syntax import macros, test, test_raises, fail, error, warn, the\nfrom unpythonic.test.fixtures import session, testset, terminate, returns_normally\n\ndef f():\n    raise RuntimeError(\"argh!\")\n\ndef g(a, b):\n    return a * b\n    fail[\"this line should be unreachable\"]\n\ncount = 0\ndef counter():\n    global count\n    count += 1\n    return count\n\nwith session(\"simple framework demo\"):\n    with testset():\n        test[2 + 2 == 4]\n        test_raises[RuntimeError, f()]\n        test[returns_normally(g(2, 3))]\n        test[g(2, 3) == 6]\n        # Use `the[]` (or several) in a `test[]` to declare what you want to inspect if the test fails.\n        # Implicit `the[]`: in comparison, the LHS; otherwise the whole expression. Used if no explicit `the[]`.\n        test[the[counter()] \u003c the[counter()]]\n\n    with testset(\"outer\"):\n        with testset(\"inner 1\"):\n            test[g(6, 7) == 42]\n        with testset(\"inner 2\"):\n            test[None is None]\n        with testset(\"inner 3\"):  # an empty testset is considered 100% passed.\n            pass\n        with testset(\"inner 4\"):\n            warn[\"This testset not implemented yet\"]\n\n    with testset(\"integration\"):\n        try:\n            import blargly\n        except ImportError:\n            error[\"blargly not installed, cannot test integration with it.\"]\n        else:\n            ... # blargly integration tests go here\n\n    with testset(postproc=terminate):\n        test[2 * 2 == 5]  # fails, terminating the nearest dynamically enclosing `with session`\n        test[2 * 2 == 4]  # not reached\n```\n\nWe provide the low-level syntactic constructs `test[]`, `test_raises[]` and `test_signals[]`, with the usual meanings. The last one is for testing code that uses conditions and restarts; see `unpythonic.conditions`.\n\nThe test macros also come in block variants, `with test`, `with test_raises`, `with test_signals`.\n\nAs usual in test frameworks, the testing constructs behave somewhat like `assert`, with the difference that a failure or error will not abort the whole unit (unless explicitly asked to do so).\n\u003c/details\u003e\n\u003cdetails\u003e\u003csummary\u003elet: expression-local variables.\u003c/summary\u003e\n\n[[docs](doc/macros.md#let-letseq-letrec-as-macros)]\n\n```python\nfrom unpythonic.syntax import macros, let, letseq, letrec\n\nx = let[[a := 1, b := 2] in a + b]\ny = letseq[[c := 1,  # LET SEQuential, like Scheme's let*\n            c := 2 * c,\n            c := 2 * c] in\n           c]\nz = letrec[[evenp := (lambda x: (x == 0) or oddp(x - 1)),  # LET mutually RECursive, like in Scheme\n            oddp := (lambda x: (x != 0) and evenp(x - 1))]\n           in evenp(42)]\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003elet-over-lambda: stateful functions.\u003c/summary\u003e\n\n[[docs](doc/macros.md#dlet-dletseq-dletrec-blet-bletseq-bletrec-decorator-versions)]\n\n```python\nfrom unpythonic.syntax import macros, dlet\n\n# In Python 3.8, use `@dlet(x \u003c\u003c 0)` instead; in Python 3.9, use `@dlet(x := 0)`\n@dlet[x := 0]  # let-over-lambda for Python\ndef count():\n    return x := x + 1  # `name := value` rebinds in the let env\nassert count() == 1\nassert count() == 2\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003edo: code imperatively in any expression position.\u003c/summary\u003e\n\n[[docs](doc/macros.md#do-as-a-macro-stuff-imperative-code-into-an-expression-with-style)]\n\n```python\nfrom unpythonic.syntax import macros, do, local, delete\n\nx = do[local[a := 21],\n       local[b := 2 * a],\n       print(b),\n       delete[b],  # do[] local variables can be deleted, too\n       4 * a]\nassert x == 84\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eAutomatically apply tail call optimization (TCO), à la Scheme/Racket.\u003c/summary\u003e\n\n[[docs](doc/macros.md#tco-automatic-tail-call-optimization-for-python)]\n\n```python\nfrom unpythonic.syntax import macros, tco\n\nwith tco:\n    # expressions are automatically analyzed to detect tail position.\n    evenp = lambda x: (x == 0) or oddp(x - 1)\n    oddp  = lambda x: (x != 0) and evenp(x - 1)\n    assert evenp(10000) is True\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eCurry automatically, à la Haskell.\u003c/summary\u003e\n\n[[docs](doc/macros.md#autocurry-automatic-currying-for-python)]\n\n```python\nfrom unpythonic.syntax import macros, autocurry\nfrom unpythonic import foldr, composerc as compose, cons, nil, ll\n\nwith autocurry:\n    def add3(a, b, c):\n        return a + b + c\n    assert add3(1)(2)(3) == 6\n\n    mymap = lambda f: foldr(compose(cons, f), nil)\n    double = lambda x: 2 * x\n    assert mymap(double, (1, 2, 3)) == ll(2, 4, 6)\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eLazy functions, a.k.a. call-by-need.\u003c/summary\u003e\n\n[[docs](doc/macros.md#lazify-call-by-need-for-python)]\n\n```python\nfrom unpythonic.syntax import macros, lazify\n\nwith lazify:\n    def my_if(p, a, b):\n        if p:\n            return a  # b never evaluated in this code path\n        else:\n            return b  # a never evaluated in this code path\n    assert my_if(True, 23, 1/0) == 23\n    assert my_if(False, 1/0, 42) == 42\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eGenuine multi-shot continuations (call/cc).\u003c/summary\u003e\n\n[[docs](doc/macros.md#continuations-callcc-for-python)]\n\n```python\nfrom unpythonic.syntax import macros, continuations, call_cc\n\nwith continuations:  # enables also TCO automatically\n    # McCarthy's amb() operator\n    stack = []\n    def amb(lst, cc):\n        if not lst:\n            return fail()\n        first, *rest = tuple(lst)\n        if rest:\n            remaining_part_of_computation = cc\n            stack.append(lambda: amb(rest, cc=remaining_part_of_computation))\n        return first\n    def fail():\n        if stack:\n            f = stack.pop()\n            return f()\n\n    # Pythagorean triples using amb()\n    def pt():\n        z = call_cc[amb(range(1, 21))]  # capture continuation, auto-populate cc arg\n        y = call_cc[amb(range(1, z+1))]\n        x = call_cc[amb(range(1, y+1))]\n        if x*x + y*y != z*z:\n            return fail()\n        return x, y, z\n    t = pt()\n    while t:\n        print(t)\n        t = fail()  # note pt() has already returned when we call this.\n```\n\u003c/details\u003e\n\n\n#### Unpythonic in 30 seconds: Language extensions with dialects\n\nThe [dialects subsystem of `mcpyrate`](https://github.com/Technologicat/mcpyrate/blob/master/doc/dialects.md) makes Python into a language platform, à la [Racket](https://racket-lang.org/). We provide some example dialects based on `unpythonic`'s macro layer. See [documentation](doc/dialects.md).\n\n\u003cdetails\u003e\u003csummary\u003eLispython: automatic TCO and an implicit return statement.\u003c/summary\u003e\n\n[[docs](doc/dialects/lispython.md)]\n\nAlso comes with automatically named, multi-expression lambdas.\n\n```python\nfrom unpythonic.dialects import dialects, Lispython  # noqa: F401\n\ndef factorial(n):\n    def f(k, acc):\n        if k == 1:\n            return acc\n        f(k - 1, k * acc)\n    f(n, acc=1)\nassert factorial(4) == 24\nfactorial(5000)  # no crash\n\nsquare = lambda x: x**2\nassert square(3) == 9\nassert square.__name__ == \"square\"\n\n# - brackets denote a multiple-expression lambda body\n#   (if you want to have one expression that is a literal list,\n#    double the brackets: `lambda x: [[5 * x]]`)\n# - local[name := value] makes an expression-local variable\ng = lambda x: [local[y := 2 * x],\n               y + 1]\nassert g(10) == 21\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003ePytkell: Automatic currying and implicitly lazy functions.\u003c/summary\u003e\n\n[[docs](doc/dialects/pytkell.md)]\n\n```python\nfrom unpythonic.dialects import dialects, Pytkell  # noqa: F401\n\nfrom operator import add, mul\n\ndef addfirst2(a, b, c):\n    return a + b\nassert addfirst2(1)(2)(1 / 0) == 3\n\nassert tuple(scanl(add, 0, (1, 2, 3))) == (0, 1, 3, 6)\nassert tuple(scanr(add, 0, (1, 2, 3))) == (0, 3, 5, 6)\n\nmy_sum = foldl(add, 0)\nmy_prod = foldl(mul, 1)\nmy_map = lambda f: foldr(compose(cons, f), nil)\nassert my_sum(range(1, 5)) == 10\nassert my_prod(range(1, 5)) == 24\ndouble = lambda x: 2 * x\nassert my_map(double, (1, 2, 3)) == ll(2, 4, 6)\n```\n\u003c/details\u003e  \n\u003cdetails\u003e\u003csummary\u003eListhell: Prefix syntax for function calls, and automatic currying.\u003c/summary\u003e\n\n[[docs](doc/dialects/listhell.md)]\n\n```python\nfrom unpythonic.dialects import dialects, Listhell  # noqa: F401\n\nfrom operator import add, mul\nfrom unpythonic import foldl, foldr, cons, nil, ll\n\n(print, \"hello from Listhell\")\n\nmy_sum = (foldl, add, 0)\nmy_prod = (foldl, mul, 1)\nmy_map = lambda f: (foldr, (compose, cons, f), nil)\nassert (my_sum, (range, 1, 5)) == 10\nassert (my_prod, (range, 1, 5)) == 24\ndouble = lambda x: 2 * x\nassert (my_map, double, (q, 1, 2, 3)) == (ll, 2, 4, 6)\n```\n\u003c/details\u003e\n\n## Install \u0026 uninstall\n\n### From PyPI\n\n```bash\npip install unpythonic\n```\n\n### From source\n\nClone the repo from GitHub. Then, navigate to it in a terminal, and:\n\n```bash\npip install . --no-compile\n```\n\nIf you intend to use the macro layer of `unpythonic`, the `--no-compile` flag is important. It prevents an **incorrect** precompilation, without macro support, that `pip install` would otherwise do at its `bdist_wheel` step.\n\nFor most Python projects such precompilation is just fine - it's just macro-enabled projects that shouldn't be precompiled with standard tools.\n\nIf `--no-compile` is NOT used, the precompiled bytecode cache may cause errors such as `ImportError: cannot import name 'macros' from 'mcpyrate.quotes'`, when you try to e.g. `from unpythonic.syntax import macros, let`. In-tree, it might work, but against an installed copy, it will fail. It has happened that my CI setup did not detect this kind of failure.\n\nThis is a common issue when using macro expanders in Python.\n\n### Uninstall\n\n```bash\npip uninstall unpythonic\n```\n\n\n## Support\n\nNot working as advertised? Missing a feature? Documentation needs improvement?\n\nIn case of a problem, see [Troubleshooting](doc/troubleshooting.md) first. Then:\n\n**[Issue reports](https://github.com/Technologicat/unpythonic/issues) and [pull requests](https://github.com/Technologicat/unpythonic/pulls) are welcome.** [Contribution guidelines](CONTRIBUTING.md).\n\nWhile `unpythonic` is intended as a serious tool for improving productivity as well as for teaching, right now my work priorities mean that it's developed and maintained on whatever time I can spare for it. Thus getting a response may take a while, depending on which project I happen to be working on.\n\n\n## License\n\nAll original code is released under the 2-clause [BSD license](LICENSE.md).\n\nFor sources and licenses of fragments originally seen on the internet, see [AUTHORS](AUTHORS.md).\n\n\n## Acknowledgements\n\nThanks to [TUT](http://www.tut.fi/en/home) for letting me teach [RAK-19006 in spring term 2018](https://github.com/Technologicat/python-3-scicomp-intro); early versions of parts of this library were originally developed as teaching examples for that course. Thanks to @AgenttiX for early feedback.\n\n\n## Relevant reading\n\nLinks to blog posts, online articles and papers on topics relevant in the context of `unpythonic` have been collected to [a separate document](doc/readings.md).\n\nIf you like both FP and numerics, we have [some examples](unpythonic/tests/test_fpnumerics.py) based on various internet sources.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechnologicat%2Funpythonic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftechnologicat%2Funpythonic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftechnologicat%2Funpythonic/lists"}