{"id":13842077,"url":"https://github.com/dmitriz/cpsfy","last_synced_at":"2025-04-05T06:08:18.262Z","repository":{"id":32428715,"uuid":"133355424","full_name":"dmitriz/cpsfy","owner":"dmitriz","description":"🚀 Tiny goodies for Continuation-Passing-Style functions, fully tested","archived":false,"fork":false,"pushed_at":"2024-11-03T12:24:48.000Z","size":2592,"stargazers_count":70,"open_issues_count":6,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-29T05:07:51.926Z","etag":null,"topics":["applicative","asynchronous","asynchronous-programming","asynchronous-tasks","callback","callback-manager","composition","continuation","continuation-passing","continuation-passing-style","continuation-tasks","control-flow","curried-functions","functional-programming","functor","monad","point-free","reducer","stream","variadic"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/cpsfy","language":"JavaScript","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/dmitriz.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","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,"publiccode":null,"codemeta":null}},"created_at":"2018-05-14T12:11:43.000Z","updated_at":"2025-02-11T21:57:50.000Z","dependencies_parsed_at":"2024-01-22T00:23:59.615Z","dependency_job_id":"92f879f7-609d-408f-ae45-3237ee1fac37","html_url":"https://github.com/dmitriz/cpsfy","commit_stats":null,"previous_names":[],"tags_count":144,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmitriz%2Fcpsfy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmitriz%2Fcpsfy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmitriz%2Fcpsfy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmitriz%2Fcpsfy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dmitriz","download_url":"https://codeload.github.com/dmitriz/cpsfy/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247294539,"owners_count":20915340,"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":["applicative","asynchronous","asynchronous-programming","asynchronous-tasks","callback","callback-manager","composition","continuation","continuation-passing","continuation-passing-style","continuation-tasks","control-flow","curried-functions","functional-programming","functor","monad","point-free","reducer","stream","variadic"],"created_at":"2024-08-04T17:01:26.965Z","updated_at":"2025-04-05T06:08:18.246Z","avatar_url":"https://github.com/dmitriz.png","language":"JavaScript","readme":"[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://war.ukraine.ua)\n```\n                                //  ) )     \n    ___      ___      ___   __//__         \n  //   ) ) //   ) ) ((   ) ) //   //   / / \n //       //___/ /   \\ \\    //   ((___/ /  \n((____   //       //   ) ) //        / /   \n\n```\n(Generated with http://patorjk.com/software/taag)\n\n# cpsfy\n\n[![npm version](https://img.shields.io/npm/v/cpsfy.svg?logo=npm)](http://npm.im/cpsfy)\n[![install size](https://packagephobia.now.sh/badge?p=cpsfy)](https://packagephobia.now.sh/result?p=cpsfy)\n[![bundlephobia](https://img.shields.io/bundlephobia/minzip/cpsfy.svg)](https://bundlephobia.com/result?p=cpsfy)\n[![Github workflow](https://github.com/dmitriz/cpsfy/actions/workflows/test.yml/badge.svg)](https://github.com/dmitriz/cpsfy/actions)\n[![CircleCI](https://circleci.com/gh/dmitriz/cpsfy.svg?style=svg)](https://circleci.com/gh/dmitriz/cpsfy)\n[![Depfu](https://badges.depfu.com/badges/7f4dd00fbcaa502c2c104ad415223506/status.svg)](https://depfu.com/github/dmitriz/cpsfy)\n[![Known Vulnerabilities](https://snyk.io/test/github/dmitriz/cpsfy/badge.svg)](https://snyk.io/test/github/dmitriz/cpsfy)\u003c!-- [![Renovate badge](https://badges.renovateapi.com/github/dmitriz/cpsfy)](https://renovatebot.com/dashboard#dmitriz/cpsfy) --\u003e\n[![Coverage Status](https://coveralls.io/repos/github/dmitriz/cpsfy/badge.svg)](https://coveralls.io/github/dmitriz/cpsfy)\n[![codecov](https://codecov.io/gh/dmitriz/cpsfy/branch/master/graph/badge.svg)](https://codecov.io/gh/dmitriz/cpsfy)\n[![CodeFactor](https://www.codefactor.io/repository/github/dmitriz/cpsfy/badge)](https://www.codefactor.io/repository/github/dmitriz/cpsfy)\n[![codebeat badge](https://codebeat.co/badges/2480c750-f9ea-46c0-a574-5e72dad17a4f)](https://codebeat.co/projects/github-com-dmitriz-cpsfy-master)\n[![Maintainability](https://api.codeclimate.com/v1/badges/a55f3fd9a13396325671/maintainability)](https://codeclimate.com/github/dmitriz/cpsfy/maintainability)\n[![DeepScan grade](https://deepscan.io/api/teams/3918/projects/5693/branches/44286/badge/grade.svg)](https://deepscan.io/dashboard#view=project\u0026tid=3918\u0026pid=5693\u0026bid=44286)\n[![GitHub last commit](https://img.shields.io/github/last-commit/dmitriz/cpsfy.svg?logo=github)](https://github.com/dmitriz/cpsfy/commits/master)\n[![npm downloads](https://img.shields.io/npm/dt/cpsfy.svg?logo=npm)](https://www.npmjs.com/package/cpsfy)\n[![MIT License](https://img.shields.io/npm/l/cpsfy.svg?color=blue)](http://opensource.org/licenses/MIT)\n\u003c!-- [![dependencies](https://david-dm.org/dmitriz/cpsfy.svg)](https://david-dm.org/dmitriz/cpsfy)  --\u003e\u003c!-- [![devDependencies](https://badgen.now.sh/david/dev/dmitriz/cpsfy)](https://david-dm.org/dmitriz/cpsfy?type=dev) --\u003e\u003c!-- [![Codacy Badge](https://api.codacy.com/project/badge/Grade/b530e9fcaac446a19d8172962b486b36)](https://app.codacy.com/app/dmitri14_3131/cpsfy?utm_source=github.com\u0026utm_medium=referral\u0026utm_content=dmitriz/cpsfy\u0026utm_campaign=Badge_Grade_Dashboard) --\u003e\n\n\nTiny but powerful goodies for Continuation-Passing-Style (CPS) functions with functional composability backed by category theory foundations.\n\nNEW: [Listen to the NotebookLM generated podcast](\nhttps://notebooklm.google.com/notebook/1647f150-a773-4c38-bfc7-8c13ef4af66d/audio)\n\n```sh\nnpm install cpsfy\n```\n\n(Or `pnpm install cpsfy` to [save disc space](https://github.com/pnpm/pnpm).)\n\n*No dependency policy.*\nFor maximum security, this package is intended to be kept minimal and transparent with **no dependencies ever**.\n\n\n## Quick demo\nWe want to read the content of `name.txt` into string `str` and remove spaces from both ends of `str`. If the resulting `str` is nonempty, \nwe read the content of the file with that name into string `content`, otherwise do nothing.\nFinally we split the `content` string into array of lines.\nIf there are any errors on the way, we want to handle them at the very end\nin a separate function without any change to our main code.\n```js\n//function returning CPS function with 2 callbacks\nconst readFileCps = file =\u003e (onRes, onErr) =\u003e  \n  require('fs').readFile(file, (err, content) =\u003e {\n    err ? onErr(err) : onRes(content)\n  })\n\n// the provided `CPS` operator wraps a CPS function to provide the API methods\nconst getLines = CPS(readFileCps('name.txt'))\n  // map applies function to the file content\n  .map(file =\u003e file.trim()) \n  .filter(file =\u003e file.length \u003e 0)\n  // chain applies function that returns a CPS function\n  .chain(file =\u003e readFileCps(file))\n  .map(text =\u003e text.split('\\n'))\n// =\u003e CPS function with 2 callbacks\n\n// To use, simply pass callbacks in the same order\ngetLines(\n  lines =\u003e console.log(lines),  // onRes callback\n  err =\u003e console.error(err)  // onErr callback\n)\n```\nNote how we handle error at the end without affecting the main logic!\n\n\n### But can't I do it with promises?\nOk, let us have another example where you can't.:wink:\nReading from static files is easy but boring.\nData is rarely static. \nWhat if we have to react to data changing in real time?\nLike our file names arriving as data stream? \nLet us use [the popular websocket library](https://github.com/websockets/ws):\n```js\nconst WebSocket = require('ws')\n// general purpose CPS function listening to websocket\nconst wsMessageListenerCps = url =\u003e cb =\u003e \n  new WebSocket(url).on('message', cb)\n```\nAnd here is the crux: \n\u003e`wsMessageListenerCps(url)` is just another CPS function!\n\nSo we can simply drop it instead of `readFileCps('name.txt')` into exactly the same code and be done with it:\n```js\nconst getLinesFromWS = CPS(wsMessageListenerCps(someUrl))\n  .map(file =\u003e file.trim()) \n  .filter(file =\u003e file.length \u003e 0)\n  .chain(file =\u003e readFileCps(file))\n  .map(text =\u003e text.split('\\n'))\n\n```\nThe new CPS function has only one callback, while the old one had two! \nYet we have used exactly the same code!\nHow so? Because we haven't done anything to other callbacks.\nThe only difference is in how the final function is called - with one callback instead of two. \nAs `wsMessageListenerCps(url)` accepts one callback, so does `getLinesFromWS` when we call it:\n```js\ngetLinesFromWS(lines =\u003e console.log(lines))\n```\nThat will print all lines for all files whose names we receive from our websocket.\nAnd if we feel overwhelmed and only want to see lines \ncontaining say \"breakfast\", nothing can be easier:\n```js\n// just add a filter\nconst breakfastLines = CPS(getLinesFromWS)\n  .filter(line =\u003e /[Bb]reakfast/.test(line))\n// call it exactly the same way\nbreakfastLines(lines =\u003e console.log(lines))\n```\nAnd from now on we'll never miss a breakfast.:smile:\n\n\n## CPS function\nAny function\n```js\nconst cpsFn = (cb1, cb2, ...) =\u003e { ... } \n```\nthat expects to be called with several (possibly zero) functions (callbacks) as arguments. The number of callbacks may vary each time `cpsFn` is called. Once called and running, `cpsFn` may call any of its callbacks any (possibly zero) number of times with any number `m` of arguments `(x1, ..., xm)`, where `m` may also vary from call to call. The `m`-tuple (vector) `(x1, ..., xm)` is regarded as the *output* of `cpsFn` from the `n`th callback `cbn`:\n```js\n// (x1, ..., xm) becomes output from nth callback whenever\ncbn(x1, ..., xm)  // is called, where n = 1, 2, ..., m\n```\nIn other words, a CPS function receives any number of callbacks that it may call in any order any number of times at any moments immediately or in the future with any number of arguments.\n\n\n## API in brief\n```js\nconst { map, chain, filter, scan, ap, CPS, pipeline } \n  = require('cpsfy')\n```\nEach of the `map`, `chain`, `filter`, `scan` operators can be used in 3 ways:\n```js\n// 'map' as curried function\nmap(f)(cpsFn)\n// 'map' method provided by the 'CPS' wrapper\nCPS(cpsFn).map(f)\n// 'cpsFn' is piped into 'map(f)' via 'pipeline' operator\npipeline(cpsFn)(map(f))\n```\nThe wrapped CPS function `CPS(cpsFn)` has all operators available as methods, while it remains a plain CPS function, i.e. can be called with the same callbacks:\n```js\nCPS(cpsFn)(f1, f2, ...) // is equivalent to\ncpsFn(f1, f2, ...)\n```\n\n### `pipeline(...arguments)(...functions)`\nPass any number of arguments to a sequence of functions,\none after another, similar to the UNIX pipe \n`(x1, ..., xn) | f1 | f2 | ... | fm`.\nAllows to write functional composition in \nthe intiutive linear way.\n\n#### Examples of `pipeline`\n```js\npipeline(1, 2)(\n  (x, y) =\u003e x + y,\n  sum =\u003e sum * 2,\n  doubleSum =\u003e -doubleSum\n) //=\u003e -6\n```\n\n#### chaining\nThe `CPS` factory provides the same methods for simple chaining:\n```js\nCPS(cpsFn).map(f).chain(g).filter(h)\n```\nHowever, the need to wrap a plain function might feel as overhead.\nThe `pipeline` operator allows to write the same code \nin functional style without the need to wrap:\n```js\n// pass cpsFn directly as argument\npipeline(cpsFn)(\n  map(f),\n  chain(g),\n  filter(h)\n)\n```\nBut the most important advantage of the `pipeline` style is\nthat you can drop there **arbitrary functions** without \nany need to patch object prototypes.\nFor instance, using the above example,\nwe can start our pipe with `url` or even insert\nsome intermediate function to compute the correct url for us:\n```js\npipeline(path)(               // begin with path\n  path =\u003e 'https://' + path  // compute url on the spot\n  url =\u003e {console.log(url); return url} // check for debugging\n  wsMessageListenerCps,       // return CPS function\n  map(f),                     // use CPS operators as usual\n  chain(g),\n  filter(h)\n)\n\n```\n\n\n### `map(...functions)(cpsFunction)`\n```js\n// these are equivalent\nmap(f1, f2, ...)(cpsFn)\nCPS(cpsFn).map(f1, f2, ...)\npipeline(cpsFn)(map(f1, f2, ...))\n```\nFor each `n`, apply `fn` to each output from the `n`th callback of `cpsFn`.\n\n#### Result of applying `map`\nNew CPS function that calls its `n`th callback `cbn` as\n```js\ncbn(fn(x1, x2, ...))\n```\nwhenever `cpsFn` calls its `n`th callback.\n\n#### Example of `map`\nUsing `readFileCps` as above.\n```js\n// read file and convert all letters to uppercase\nconst getCaps = map(str =\u003e str.toUpperCase())(\n  readFileCps('message.txt')\n)\n// or\nconst getCaps = CPS(readFileCps('message.txt'))\n  .map(str =\u003e str.toUpperCase())\n// or\nconst getCaps = pipeline(readFileCps('message.txt'))(\n  map(str =\u003e str.toUpperCase())\n)\n\n// getCaps is CPS function, call with any callback\ngetCaps((err, data) =\u003e err \n  ? console.error(err) \n  : console.log(data)\n) // =\u003e file content is capitalized and printed\n```\n\n### `chain(...functions)(cpsFunction)`\n```js\n// these are equivalent\nchain(f1, f2, ...)(cpsFn)\nCPS(cpsFn).chain(f1, f2, ...)\npipeline(cpsFn)(chain(f1, f2, ...))\n```\nwhere each `fn` is a curried function\n```js\n// fn is expected to return a CPS function\nconst fn = (x1, x2, ...) =\u003e (cb1, cb2, ...) =\u003e { ... }\n```\nThe `chain` operator applies each `fn` to each output from the `n`th callback of `cpsFn`, however, the CPS *ouptup* of `fn` is passed ahead instead of the return value. \n\n#### Result of applying `chain`\nNew CPS function `newCpsFn` that calls `fn(x1, x2, ...)` whenever `cpsFn` passes output `(x1, x2, ...)` into its `n`th callback, and collects all outputs from all callbacks of all `fn`s. Then for each fixed `m`, outputs from the `m`th callbacks of all `fn`s are collected and passed into the `m`th callback `cbm` of `newCpsFn`:\n```js\ncbm(y1, y2, ...)  // is called whenever \ncbmFn(y1, y2, ...)  // is called where\n// cbmFn is the mth callback of fn\n```\n\n#### Example of `chain`\nUsing `readFileCps` as above.\n```js\n// write version of readFileCps\nconst writeFileCps = (file, content) =\u003e (onRes, onErr) =\u003e  \n  require('fs').writeFile(file, content, (err, message) =\u003e {\n    err ? onErr(err) : onRes(message)\n  })\n\nconst copy = chain(\n  // function that returns CPS function\n  text =\u003e writeFileCps('target.txt', text)\n)(\n  readFileCps('source.txt')  // CPS function\n)\n// or as method\nconst copy = CPS(readFileCps('source.txt'))\n  .chain(text =\u003e writeFileCps('target.txt', text))\n// or with pipeline operator\nconst copy = pipeline(readFileCps('source.txt'))(\n  chain(text =\u003e writeFileCps('target.txt', text))\n)\n\n// copy is a CPS function, call it with any callback\ncopy((err, data) =\u003e err \n  ? console.error(err) \n  : console.log(data)\n) // =\u003e file content is capitalized and printed\n```\n\n\n### `filter(...predicates)(cpsFunction)`\n```js\n// these are equivalent\nfilter(pred1, pred2, ...)(cpsFn)\nCPS(cpsFn).filter(pred1, pred2, ...)\npipeline(cpsFn)(filter(pred1, pred2, ...))\n```\nwhere each `predn` is the `n`th predicate function used to filter output from the `n`th callback of `cpsFn`. \n\n#### Result of applying `filter`\nNew CPS function that calls its `n`th callback `cbn(x1, x2, ...)` whenever `(x1, x2, ...)` is an output from the `n`th callback of `cpsFun` and\n```js\npredn(x1, x2, ...) == true\n```\n\n#### Example of `filter`\nUsing `readFileCps` and `writeFileCps` as above.\n```js\n// only copy text if it is not empty\nconst copyNotEmpty = CPS(readFileCps('source.txt'))\n  .filter(text =\u003e text.length \u003e 0)\n  .chain(text =\u003e writeFileCps('target.txt', text))\n\n// copyNotEmpty is CPS function, call with any callback\ncopyNotEmpty(err =\u003e console.error(err))\n```\n\n### `scan(...initStates)(...reducers)(cpsFunction)`\nSimilar to [`reduce`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce), except that each accumulated values is passed into its callback whenever there is new output.\n```js\n// these are equivalent\nscan(ini1, init2, ...)(red1, red2, ...)(cpsFn)\n(cpsFn).scan(ini1, init2, ...)(red1, red2, ...)\npipeline(cpsFn)(scan(ini1, init2, ...)(red1, red2, ...))\n```\nwhere each `redn` is a *reducer* and `init` is the initial accumulated value.\n```js\n// compute new accumulator value from the old one \n// and the tuple of current values (y1, y2, ...)\nconst redn = (acc, y1, y2, ...) =\u003e ... \n```\n\n#### Result of applying `scan`\nNew CPS function whose function whose outputs are the accumulated values. \nFor each output `(y1, y2, ...)` from the `n`th callback, \nthe `n`th reducer `redn` is used to compute the new acculated value \n`redn(acc, y1, y2, ...)`, where `acc` starts with the `n`th initial state `initStaten`, similar to `reduce`.\n\n\n#### Example of `scan`\n```js\n// CPS function with 2 callbacks, clicking on one\n// of the buttons sends '1' into respective callback\nconst getVotes = (onUpvote, onDownvote) =\u003e {\n  upvoteButton.addEventListener('click', \n    ev =\u003e onUpvote(1)\n  )\n  downvoteButton.addEventListener('click', \n    ev =\u003e onDownvote(1)\n  )  \n}\n// count numbers of up- and downvotes and \n// pass into respective callbacks\nconst countVotes = CPS(getVotes)\n  .scan(0,0)(\n    ([up, down], upvote) =\u003e [up + upvote, down], \n    ([up, down], downvote) =\u003e [up, down + downvote]\n   )\n\n// countVotes is CPS function that we can call \n// with any callback\ncountVotes(\n  upvotes =\u003e console.log('Total upvotes: ', upvotes),\n  downvotes =\u003e console.log('Total downvotes: ', downvotes),\n)\n```\n\n\n### `ap(...cpsFunctions)(cpsFunction)`\nSee [running CPS functions in parallel](DOCUMENTATION.md#running-cps-functions-in-parallel).\nInspired by the Applicative Functor interface, see e.g. https://funkia.github.io/jabz/#ap\n\n### `lift(...functions)(cpsFunction)` (TODO)\nSee [lifting functions of multiple arguments](DOCUMENTATION.md#lifting-functions-of-multiple-parameters)\nThe \"sister\" of `ap`, apply functions with multiple arguments to\noutputs of CPS functions running in parallel, derived from `ap`,\nsee e.g. https://funkia.github.io/jabz/#lift\n\n### `merge(...cpsFunctions)` (TODO)\nSee [`CPS.merge`](DOCUMENTATION.md#cpsmerge-todo).\nMerge outputs from multiple CPS functions, separately in each callback.\nE.g. separately merge results and errors from multiple promises\nrunning in parallel.\n\n## More details?\nThis `README.md` is kept minimal to reduce the package size. For more human introduction, motivation, use cases and other details, please see [DOCUMENTATION](DOCUMENTATION.md).\n\n## License\nMIT © [Dmitri Zaitsev](https://github.com/dmitriz)\n","funding_links":[],"categories":["JavaScript (485)","JavaScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmitriz%2Fcpsfy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdmitriz%2Fcpsfy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmitriz%2Fcpsfy/lists"}