{"id":22288337,"url":"https://github.com/unclebob/functor","last_synced_at":"2025-07-28T22:32:37.583Z","repository":{"id":219637980,"uuid":"749517597","full_name":"unclebob/functor","owner":"unclebob","description":"experimental macro for allowing Algol-like block structure scoping in clojure","archived":false,"fork":false,"pushed_at":"2024-02-06T19:58:22.000Z","size":47,"stargazers_count":19,"open_issues_count":1,"forks_count":3,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-02-07T18:56:00.775Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/unclebob.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2024-01-28T19:58:26.000Z","updated_at":"2024-02-07T18:56:00.776Z","dependencies_parsed_at":"2024-02-06T19:01:08.758Z","dependency_job_id":null,"html_url":"https://github.com/unclebob/functor","commit_stats":null,"previous_names":["unclebob/functor"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unclebob%2Ffunctor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unclebob%2Ffunctor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unclebob%2Ffunctor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/unclebob%2Ffunctor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/unclebob","download_url":"https://codeload.github.com/unclebob/functor/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":227961828,"owners_count":17847841,"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":[],"created_at":"2024-12-03T17:04:12.705Z","updated_at":"2024-12-03T17:04:13.031Z","avatar_url":"https://github.com/unclebob.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# functor\nThis is a proposal for a new language feature within Clojure (See the end of this document).  I've been experimenting with this proposed feature by creating the `functor` macro that simulates what I'd like the language to do for me.  \n\n## Installation\n    project.clj [com.cleancoder/functor \"0.0.1-SNAPSHOT\"]\n    require [functor.core :refer [functor]]\n\n## Introduction\nThe functor macro is an experiment in improving the cleanliness and refactorability of Clojure programs. \n\nHave you ever written a Clojure function and then wanted to refactor it by extracting it into parts?\nDid you find that experience frustring because `letfn` is ugly and nesting `lets` is even uglier?\nMe too.  In fact that's my biggest complaint about Clojure.\n\nThe problem is that Clojure does not offer a way to scope a set of variables into subfunctions. This means that it is difficult, if not impossible, to extract subfunctions that have access to the parent functions variables.  Clojure does offer `letfn`, or just `def f fn...` but these forms do not allow subfunctions to initialize variables at the outer scope.\n\nThe `functor` macro is an experiment to see if extractions can be made easier by providing\nan Algol-like block structure.  This will allow programmers to create sub-functions that have access to all\nthe local variables and arguments used by the parent function. \n\nIf this sounds to OO, don't worry.  I'm not proposing we add classes or mutated variables to the language.  Rather I'm proposing a mechanism for initializing outer-scope variables within subfunction.  The difference is significant.  This could be implemented by the state monad. \n\n## Starting Example\n\nThe following is a simple function for calculating the roots of a quadratic equation.  \n\n\t(defn quad- [a b c]\n\t  (let [discriminant (- (* b b) (* 4 a c))]\n\t    (cond\n\t      (zero? a)\n\t      (/ (- c) b)\n\n\t      (zero? discriminant)\n\t      (/ (- b) (* 2 a))\n\n\t      (neg? discriminant)\n\t      (let [i-sqrt-discriminant (Math/sqrt (- discriminant))\n\t            c-x1 [(- b) i-sqrt-discriminant]\n\t            c-x1 (map #(/ % (* 2 a)) c-x1)\n\t            c-x2 [(- b) (- i-sqrt-discriminant)]\n\t            c-x2 (map #(/ % (* 2 a)) c-x2)]\n\t        [c-x1 c-x2])\n\t      :else\n\t      (let [sqrt-desc (Math/sqrt discriminant)\n\t            x1 (/ (+ (- b) sqrt-desc) (* 2 a))\n\t            x2 (/ (- (- b) sqrt-desc) (* 2 a))]\n\t        [x1 x2]))))\n\t\t\t\nThis doesn't look to bad.  It's a nicely subdivided into the relevant parts.  Still, it's a bit large, and lacks explanation.  \n\nHere is how it looks with the functor macro.  \n\n\t(def quad\n\t  (functor\n\t    ([a b c]\n\t     (\u003c- discriminant (make-discriminant))\n\t     (cond\n\t       (zero? a) (linear)\n\t       (zero? discriminant) (one-root)\n\t       (neg? discriminant) (complex-roots)\n\t       :else (real-roots)))\n\n\t    (linear [] (/ (- c) b))\n\t    (one-root [] (/ (- b) (* 2 a)))\n\t\n\t    (over-2a [c] (map (fn [x] (/ x (* 2 a))) c))\n\t\n\t    (complex-roots\n\t      []\n\t      (let [i-sqrt-discriminant (Math/sqrt (- discriminant))\n\t            c-x1 (over-2a [(- b) i-sqrt-discriminant])\n\t            c-x2 (over-2a [(- b) (- i-sqrt-discriminant)])]\n\t        [c-x1 c-x2]))\n\n\t    (real-roots\n\t      []\n\t      (let [sqrt-desc (Math/sqrt discriminant)\n\t            x1 (/ (+ (- b) sqrt-desc) (* 2 a))\n\t            x2 (/ (- (- b) sqrt-desc) (* 2 a))]\n\t        [x1 x2]))\n\n\t    (make-discriminant [] (- (* b b) (* 4 a c)))))\n\t\nNotice that now we have a set of sub-functions that are scoped within the outer `quad` function.  \nNotice also that those sub-functions have access to the arguments of `quad` _and_ to the `discriminant` \nvariable.  This is similar to the `label` feature of common lisp.\n\nNote the strange `\u003c-` symbol that _sets_ the `discriminant`.  Variables that are initialized in that manner are \nnot global.  They are not known outside the `quad` function but are usable _throughout the scope_ of that function.\nIn that sense they are something like instance variables of a class.  Indeed, that's why I chose the name _functor_.\nOne definition of a functor is a class with one public method and some instance variables.\n\nThe example above is illustrative, but not motivating.  There are other, better ways to organize the `quad`\nfunction.  But before we get into better motivations, let's look at how the `functor` macro works.\n\n## Behind the Scenes\nConsider this simple example:\n\n    (def mean\n      (functor\n        ([ns]\n         (make-sum)\n         (/ sum (count ns)))\n        (make-sum\n          []\n          (\u003c- sum (reduce + ns)))))\n\nAgain, this is not a particularly motivating example; but it is small enough to illustrate _how_ the `functor`\nmacro works.  Here is the (slightly trimmed) code that it generates.\n\n    (def mean \n      (fn [ns] \n        (let [sum (atom nil) \n              make-sum (fn [] (reset! sum (reduce + ns)))] \n          (make-sum) \n          (/ @sum (count ns)))))\n\nEw, Yuk!  An `atom`!  Allow me to explain.\nThe `sum` atom is scoped within the `mean` function.  All the sub-functions have access to `sum`, \nand _can set_ it.  This means that nobody outside of `mean` has access to `sum`, but all the sub-functions\nwithin `mean` do.  That was the goal I was aiming for.\n\nThe choice to use `atom`s for the scoped variables was a pragmatic compromise.  My original goal was to thread a map\ncontaining all the scoped variables throughout all the sub-functions.  (i.e. the State Monad).  But that\nincreased the complexity of the macro beyond the effort I was willing to give it.  In the end the `atom`s create\na small risk of concurrent mutation -- but I'm willing to accept that for benefit of being able to refactor\notherwise unrefactorable functions.\n\nAlso, I build this macro as a means for me to experiment with what I hope may become a new language feature.\n\n## A Better Example\nSo, on to a more motivating example.  Here's a small function from `github.com/unclebob/more-speech`.\n\n    (defn add-cross-reference [db event]\n      (let [[_ _ referent] (events/get-references event)\n            id (:id event)]\n        (when (some? referent)\n          (if (gateway/event-exists? db referent)\n            (gateway/add-reference-to-event db referent id)\n            (update-mem [:orphaned-replies referent] conj id)))\n        (let [orphaned-replies (get-mem [:orphaned-replies id])]\n          (doseq [orphan orphaned-replies]\n            (gateway/add-reference-to-event db id orphan))\n          (update-mem :orphaned-replies dissoc id))))\n\nThis really isn't too bad; but can we make this better with a functor?\n\n    (def add-cross-reference\n      (functor\n        ([db event]\n         (\u003c- id (:id event))\n         (\u003c- parent (nth (events/get-references event) 2))\n         \n         (when (some? parent)\n           (adopt-or-orphan-this-event))\n         (un-orphan-events-that-reference-this-event))\n    \n        (adopt-or-orphan-this-event\n          []\n          (if (gateway/event-exists? db parent)\n            (gateway/add-reference-to-event db parent id)\n            (update-mem [:orphaned-replies parent] conj id)))\n        \n        (un-orphan-events-that-reference-this-event\n          []\n          (let [orphaned-replies (get-mem [:orphaned-replies id])]\n            (doseq [orphan orphaned-replies]\n              (gateway/add-reference-to-event db id orphan))\n            (update-mem :orphaned-replies dissoc id)))))\n\nThis is a bit better.  I could have done it with `letfn` but this seems a bit prettier to me.\nBut is there a better example?\n\n## Even Better Example\n\nAgain, from more-speech, there's this:\n\n    (defn encrypt-if-direct-message [content tags]\n      (if (re-find #\"^D \\#\\[\\d+\\]\" content)\n        (let [reference-digits (re-find #\"\\d+\" content)\n              reference-index (Integer/parseInt reference-digits)\n              p-tag (get tags reference-index)]\n          (if (nil? p-tag)\n            [content 1]\n            (let [recipient-key (hex-string-\u003enum (second p-tag))\n                  private-key (get-mem [:keys :private-key])\n                  sender-key (hex-string-\u003enum private-key)\n                  shared-secret (SECP256K1/calculateKeyAgreement sender-key recipient-key)\n                  encrypted-content (SECP256K1/encrypt shared-secret content)]\n              [encrypted-content 4])))\n        [content 1]))\n\nNested `if`s with nested `let`s.  Urghh.  Let's see how a functor might help.\n\n    (def encrypt-if-direct-message\n      (functor\n        ([content tags]\n         (if (is-direct?)\n           (encrypt-if-properly-referenced)\n           (leave-unencrypted)))\n    \n        (is-direct? [] (re-find #\"^D \\#\\[\\d+\\]\" content))\n    \n        (get-p-tag-from-content\n          []\n          (let [reference-digits (re-find #\"\\d+\" content)\n                reference-index (Integer/parseInt reference-digits)]\n            (get tags reference-index)))\n    \n        (encrypt-content\n          []\n          (let [recipient-key (hex-string-\u003enum (second p-tag))\n                private-key (get-mem [:keys :private-key])\n                sender-key (hex-string-\u003enum private-key)\n                shared-secret (SECP256K1/calculateKeyAgreement sender-key recipient-key)]\n            (SECP256K1/encrypt shared-secret content)))\n    \n        (encrypt-if-properly-referenced\n          []\n          (\u003c- p-tag (get-p-tag-from-content))\n          (if (nil? p-tag)\n            [content 1]\n            [(encrypt-content) 4]))\n    \n        (leave-unencrypted [] [content 1])))\n\nLook at that top level function!   If direct, encrypt otherwise leave unencrypted.  Nice.\n\nThis would be tougher to do with `letfn` because you'd have to pass that `p-tag` around.  Allowing `p-tag` to\nbe set by one sub-function but used by another is -- convenient.\n\n## Yet Another Example\nThis one's got a lot of nested `let`s.  So lets see what the functor can do to untangle it.\n\n\t(defn handle-duplicate-event [event id relays-already-sent-this-id url]\n\t  (do\n\t    (update-mem [:event-counter :dups] inc-if-nil)\n\t    (when (is-text-event? event)\n\t      (when-not (contains? relays-already-sent-this-id url)\n\t        (update-mem [:processed-event-ids id] add-relay-to-processed-event-ids url)\n\t        (gateway/add-relays-to-event (get-db) id [url])))))\n\n\t(defn validate-and-process-event [url envelope]\n\t  (let [[_name _subscription-id inner-event :as _decoded-msg] envelope\n\t        event (translate-event inner-event)\n\t        id (:id event)\n\t        relays-already-sent-this-id (get-mem [:processed-event-ids id])]\n\t    (update-mem [:event-counter :kinds (:kind event)] inc-if-nil)\n\t    (if (some? relays-already-sent-this-id)\n\t      (handle-duplicate-event event id relays-already-sent-this-id url)\n\t      (let [computed-id (compute-id inner-event)\n\t            ui-handler (get-mem :event-handler)]\n\t        (update-mem :processed-event-ids assoc id #{url})\n\t        (if (= id computed-id)\n\t          (let [event (decrypt-dm-event event)]\n\t            (when (not (:private event))\n\t              (process-event event url)\n\t              (when (is-text-event? event)\n\t                (handle-text-event ui-handler event))))\n\t          (log-pr 2 'id-mismatch url 'computed-id (util/num32-\u003ehex-string computed-id) envelope))))))\n\t\t\t  \nUsing the functor macro I was able to cut out most of the nested `let`s, rearrange the code to make it \"saner\", and combine the two related functions into a single main function with several sub-functions.  The result is, in my humble opinion, much better.\n\n\t(def validate-and-process-event\n\t  (functor\n\t    ([url envelope]\n\t     (init)\n\t     (if (is-duplicate-id?)\n\t       (handle-duplicate-event)\n\t       (handle-new-event)))\n\n\t    (handle-duplicate-event\n\t      []\n\t      (update-mem [:event-counter :dups] inc-if-nil)\n\t      (when (is-text-event? event)\n\t        (when-not (contains? relays-already-sent-this-id url)\n\t          (update-mem [:processed-event-ids id] add-relay-to-processed-event-ids url)\n\t          (gateway/add-relays-to-event (get-db) id [url]))))\n\n\t    (is-duplicate-id? [] (some? relays-already-sent-this-id))\n\n\t    (is-valid-id?\n\t      []\n\t      (\u003c- computed-id (compute-id inner-event))\n\t      (= id computed-id))\n\n\t    (handle-valid-event\n\t      []\n\t      (let [decrypted-event (decrypt-dm-event event)]\n\t        (when (not (:private decrypted-event))\n\t          (process-event decrypted-event url)\n\t          (when (is-text-event? decrypted-event)\n\t            (handle-text-event ui-handler decrypted-event)))))\n\n\t    (handle-new-event\n\t      []\n\t      (update-mem :processed-event-ids assoc id (set [url]))\n\t      (if (is-valid-id?)\n\t        (handle-valid-event)\n\t        (log-pr 2 'id-mismatch url 'computed-id (util/num32-\u003ehex-string computed-id) envelope)))\n\n\t    (init []\n\t          (\u003c- ui-handler (get-mem :event-handler))\n\t          (\u003c- inner-event (nth envelope 2))\n\t          (\u003c- event (translate-event inner-event))\n\t          (\u003c- id (:id event))\n\t          (\u003c- relays-already-sent-this-id (get-mem [:processed-event-ids id]))\n\t          (update-mem [:event-counter :kinds (:kind event)] inc-if-nil)\n\t          )\n\t    )\n\t  )\n\n## Issues with the macro\nMy IDE (IntelliJ) hates this.  It sees all the symbols within the functor as undefined, and colors them all orange.  Yuk!\nIt also won't rename them or let me refactor them nicely.  Damn.\n\nAlso, the macro does not like `#()` and `#{}` reader shortcuts.  You have to replace them with `fn` and `set`.  I don't know why.\n\n## Proposal\nI think this would make a nice addition to the language.  It would make it much easier to pull complex functions\napart into nicely named sub-functions while allowing them to communicate with variables scoped to the outer\nfunction.  \n\nThe language feature would not need to use `atom`s because the compiler could use vars that are scoped \nto the outer function. \n\nI'd like it to be part of `fn` (and therefore `defn` and `letfn`) and look something like this:\n\n      (defn mean\n\t\t([ns]\n\t\t (make-sum)\n\t\t (/ sum (count ns)))\n\t\t(make-sum []\n\t\t  (\u003c- sum (reduce + ns))))\n\n## Argument\nI expect that some will react against this by saying (or thinking) \n\u003e _\"Oh, no, OO!  We don't want to bring _objects_ into Clojure.\"_  \n\nBut after some thought I think those folks will agree that this is _not_ what's going on here.  We are not creating some long lived mutable object.  Rather, we are creating vars scoped at the level of a function with sub-functions that have access to those vars.  From a functional point of view this is no different from having a set of `let` vars followed by a set of expressions that use those vars. \n\nOne might argue that the `\u003c-` operator allows vars to be mutated.  Again, that is not necessarily the case.  In most cases the vars are simply initialized once and never mutated.  The compiler could ensure that no uninitialized var was used.  The compiler could prevent mutation by simply preventing more than one execution of `\u003c-` on a var.  Better than that, however, would be to simulate the behavior of `let` redefinitions of a var, which are _not_ mutations. \n\t \n\t(let [x 1\n\t      x 2]\n\t   ...)\n\nIn the above, `x` is not mutated.  Instead a new `x` var is created and the older one is hidden.  Any function holding on to the older one still has access to it.  The vars set by `\u003c-` could have the same behavior.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Funclebob%2Ffunctor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Funclebob%2Ffunctor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Funclebob%2Ffunctor/lists"}