{"id":13726192,"url":"https://github.com/jonlaing/rationale","last_synced_at":"2026-03-13T20:37:28.785Z","repository":{"id":46938797,"uuid":"111003286","full_name":"jonlaing/rationale","owner":"jonlaing","description":"Ramda inspired library of helper functions for ReasonML","archived":false,"fork":false,"pushed_at":"2023-01-03T18:23:11.000Z","size":492,"stargazers_count":275,"open_issues_count":12,"forks_count":14,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-10-01T15:17:51.313Z","etag":null,"topics":["bucklescript","lodash","monad","ramda","reasonml","utility"],"latest_commit_sha":null,"homepage":null,"language":"OCaml","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/jonlaing.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":"2017-11-16T17:46:23.000Z","updated_at":"2024-09-13T12:29:39.000Z","dependencies_parsed_at":"2023-02-01T08:46:15.546Z","dependency_job_id":null,"html_url":"https://github.com/jonlaing/rationale","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/jonlaing/rationale","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonlaing%2Frationale","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonlaing%2Frationale/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonlaing%2Frationale/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonlaing%2Frationale/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jonlaing","download_url":"https://codeload.github.com/jonlaing/rationale/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonlaing%2Frationale/sbom","scorecard":{"id":531220,"data":{"date":"2025-08-11","repo":{"name":"github.com/jonlaing/rationale","commit":"06a48fd39b106319a4a4477e90777db51c915353"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.1,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Code-Review","score":3,"reason":"Found 5/15 approved changesets -- score normalized to 3","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 20 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'","Warn: branch protection not enabled for branch '0.2.0'","Warn: branch protection not enabled for branch '0.1.10'","Warn: branch protection not enabled for branch '0.1.9'","Warn: branch protection not enabled for branch '0.1.8'","Warn: branch protection not enabled for branch '0.1.7'","Warn: branch protection not enabled for branch '0.1.6'","Warn: branch protection not enabled for branch '0.1.5'","Warn: branch protection not enabled for branch '0.1.4'","Warn: branch protection not enabled for branch '0.1.3'","Warn: branch protection not enabled for branch '0.1.1'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":0,"reason":"55 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92","Warn: Project is vulnerable to: GHSA-6chw-6frg-f759","Warn: Project is vulnerable to: GHSA-v88g-cgmw-v5xw","Warn: Project is vulnerable to: GHSA-93q8-gq69-wqmw","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c","Warn: Project is vulnerable to: GHSA-w573-4hg7-7wgq","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-8r6j-v8pm-fqw3","Warn: Project is vulnerable to: MAL-2023-462","Warn: Project is vulnerable to: GHSA-w457-6q6x-cgp9","Warn: Project is vulnerable to: GHSA-62gr-4qp9-h98f","Warn: Project is vulnerable to: GHSA-f52g-6jhx-586p","Warn: Project is vulnerable to: GHSA-2cf5-4w76-r9qv","Warn: Project is vulnerable to: GHSA-3cqr-58rm-57f8","Warn: Project is vulnerable to: GHSA-g9r4-xpmj-mj65","Warn: Project is vulnerable to: GHSA-q2c6-c6pm-g3gh","Warn: Project is vulnerable to: GHSA-765h-qjxv-5f44","Warn: Project is vulnerable to: GHSA-f2jv-r9rf-7988","Warn: Project is vulnerable to: GHSA-43f8-2h32-f4cj","Warn: Project is vulnerable to: GHSA-qqgx-2p2h-9c37","Warn: Project is vulnerable to: GHSA-896r-f27r-55mw","Warn: Project is vulnerable to: GHSA-9c47-m6qq-7p4h","Warn: Project is vulnerable to: GHSA-6c8f-qphg-qjgp","Warn: Project is vulnerable to: GHSA-jf85-cpcp-j695","Warn: Project is vulnerable to: GHSA-p6mc-m468-83gw","Warn: Project is vulnerable to: GHSA-29mw-wpgm-hmr9","Warn: Project is vulnerable to: GHSA-35jh-r3h4-6jhm","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-vh95-rmgr-6w4m","Warn: Project is vulnerable to: GHSA-xvch-5gv4-984h","Warn: Project is vulnerable to: GHSA-fhjf-83wg-r2j9","Warn: Project is vulnerable to: GHSA-5fw9-fq32-wv5p","Warn: Project is vulnerable to: GHSA-hj48-42vr-x3v9","Warn: Project is vulnerable to: GHSA-hrpp-h998-j3pp","Warn: Project is vulnerable to: GHSA-p8p7-x288-28g6","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-4g88-fppr-53pp","Warn: Project is vulnerable to: GHSA-4jqc-8m5r-9rpr","Warn: Project is vulnerable to: GHSA-3jfq-g458-7qm9","Warn: Project is vulnerable to: GHSA-r628-mhmh-qjhw","Warn: Project is vulnerable to: GHSA-9r2w-394v-53qc","Warn: Project is vulnerable to: GHSA-5955-9wpr-37jh","Warn: Project is vulnerable to: GHSA-qq89-hq3f-393p","Warn: Project is vulnerable to: GHSA-f5x3-32g6-xq36","Warn: Project is vulnerable to: GHSA-jgrx-mgxx-jf9v","Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3","Warn: Project is vulnerable to: GHSA-6fc8-4gx4-v693","Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q","Warn: Project is vulnerable to: GHSA-c4w7-xm78-47vh","Warn: Project is vulnerable to: GHSA-p9pc-299p-vxgp"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-20T05:47:31.304Z","repository_id":46938797,"created_at":"2025-08-20T05:47:31.304Z","updated_at":"2025-08-20T05:47:31.304Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30474944,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-13T17:15:31.527Z","status":"ssl_error","status_checked_at":"2026-03-13T17:15:22.394Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["bucklescript","lodash","monad","ramda","reasonml","utility"],"created_at":"2024-08-03T01:02:55.359Z","updated_at":"2026-03-13T20:37:28.748Z","avatar_url":"https://github.com/jonlaing.png","language":"OCaml","funding_links":[],"categories":["OCaml","Reason"],"sub_categories":["Libraries"],"readme":"Rationale\n================================================================================\n\nRationale is inspired by [RamdaJS](http://ramdajs.com/). It is a collection of helper utility functions that are absent in the OCaml/ReasonML standard library.\n\nNote that not all of Ramda was ported over, as many of Ramda's utilities are making up for deficits in Javascript, which Reason doesn't have. Furthermore, many of the functions that operate on objects, simply don't make sense in Reason.\n\nInstallation\n--------------------------------------------------------------------------------\n\nRun `npm install --save rationale` and add `rationale` to `bs-dependencies` in `bsconfig.json`. \n\nFeatures\n--------------------------------------------------------------------------------\n\n### Exception-free List operations\n\nIn the OCaml/ReasonML standard library, many of the common List operations throw exceptions if there's a problem. Rationale's utilities do not throw exceptions, and instead return `options`.\n\n- head\n- tail\n- init\n- last\n- nth\n- etc\n\n### Monadic Options and Belt.Results\n\nRationale includes monadic and functor operations ala Haskell for the `option` and `Belt.Result` types.\n\n```Reason\nopen Rationale.Option.Infix;\nopen Rationale.Function;\n\nRList.init(a)\n  \u003e\u003e= ((x) =\u003e RList.last(a) \u003c$\u003e f \u003c$\u003e flip(RList.append, x))\n  \u003c$\u003e RList.concat(b)\n  |\u003e Option.default(xs);\n```\n\n### Additional ADT-s\n  #### Reader\n  #### Writer\n  #### IO, KIO\n  The main idea of the IO monad is to isolate our effects as much possible. Some languages like Haskell don't even\n  allow the user to manually start an effect, which is not the case for Reason, but with a little bit of self-discipline we can handle side effects in a monadic fashion as well.\n\n  KIO is a monadic structure with the type signature: ```io('a, 'env) = IO(Lazy.t('env =\u003e 'a))```\n  which makes it posible to store some effect with its config environment. ( dependency injection )\n\n  We can use `return` or `lift` methods to wrap our unsafe mutations.\n  The difference is that with `return` we throw away the environment, while with `lift` we are using it.\n\n  As you can see in our example we used `return` to wrap `Js.log`  \n  ```reason\n  let log = (x: string) =\u003e\n    KIO.return({\n      Js.log(x)\n    });\n  ```\n\n  In the following `saveFile` example we use lift which makes it possible to use an injected `env` config\n  ( the injection happens when we call `KIO.runIO` )\n  ```reason\n  let saveFile = str =\u003e KIO.lift(env =\u003e {\n    Node.Fs.writeFileSync(env.target, str, `ascii);\n  });\n  ```\n\n  To run our effects we need to call: KIO.runIO(main, env);\n  Ideally in your program this method will be called once on the bottom of your index file.\n  Example:\n\n  ```reason\n    exception ReadError(string);\n    type envT = {\n      path: string,\n      dir: string,\n      target: string\n    };\n    let env = {path: \"./input.txt\", dir: \"/\", target: \"./out.txt\"};\n\n    let readFile = KIO.lift(env =\u003e {\n        try (Node.Fs.readFileSync(env.path, `ascii)) {\n        | ReadError(msg) =\u003e raise @@ ReadError(\"File read failed: \" ++ msg)\n        };\n      });\n\n    let saveFile = str =\u003e KIO.lift(env =\u003e {\n      Node.Fs.writeFileSync(env.target, str, `ascii);\n    });\n\n    let log = (x: string) =\u003e\n      KIO.return({\n        Js.log(x)\n      });\n\n    let parseFile = input =\u003e {\n      let l  = Js.String.split(\"\\n\", input);\n      Array.map(x =\u003e x ++ \"100\", l);\n    } \n\n    let joinArray = (xs: array(string)) =\u003e Js.Array.join(xs)\n\n    let main = KIO.(\n      readFile \n        \u003c$\u003e parseFile\n        \u003c$\u003e joinArray\n        \u003e\u003e= saveFile\n    )\n\n    KIO.runIO(main, env);\n  ```\n\n### Support for Point-free style\n\nRationale has `compose` and `pipe` functions, as well as supporting infix operators: `\u003c||` and `||\u003e` respectively.\n\n### Infix Lens composition\n\nRationale also allows for fluid lens composition via infix operators: `-\u003c\u003c` and `\u003e\u003e-`.\n\n```Reason\nLens.view(aLens \u003e\u003e- bLens \u003e\u003e- optional(0), { a: { b: Some(3) } });\n\n```\n\n### Function signatures for composition\n\nLike in Ramda, functions always keep their data at the end making piping and composing a breeze:\n\n```Reason\nlist\n  |\u003e take(9)\n  |\u003e drop(3)\n  |\u003e splitAt(4);\n```\n\nUsage\n--------------------------------------------------------------------------------\n\n### Using Optional Returns in RList and Dict\n\nReturning `option('a)` from functions is generally preferred to throwing an exception.\nIt protects you from runtime errors, and forces you to deal with potential errors at\ncompile time. However, if you're not used to doing it, things can get a little\nconfusing.\n\nPattern matching for errors all the time would be extremely cumbersome. Fortunately,\nwe provide a host of useful methods to working with optional returns. Hopefully,\nthis doc will show you that you don't need to use excessive pattern matching to\nwork with optional returns.\n\n#### Default\n\nThe most straight forward way to get out of an `option` is by calling `default`.\n\n```Reason\nOption.default(0, Some(1)); /* 1 */\n\nOption.default(0, None); /* 0 */\n```\n\n#### Monads\n\nCalling `default` will definitely get you out of the `option`, but what if you want\nto do some things to it first? What if you need other funtions that also return `option`?\n\nthe Option module includes monadic operations for `option`, so you can take a *railway oriented*\napproach to working with them.\n\nFirst, let's check if the last item of a list is equal to a certain value:\n\n```Reason\nlet lastEquals = (a, xs) =\u003e Option.fmap(Util.eq(a), RList.last(xs)) |\u003e Option.default(false);\n\nlastEquals(3, [1,2,3]); /* true */\nlastEquals(3, [4,5,6]); /* false */\nlastEquals(3, []); /* false */\n\n/* Or, with infix operators */\nopen Option.Infix;\n\nlet lastEquals = (a, xs) =\u003e RList.last(xs) \u003c$\u003e Util.eq(a) |\u003e Option.default(false);\n```\n\nHere we used `fmap` and its infix variation `\u003c$\u003e` to apply a function to the value *inside* the option.\nNote that, `Util.eq` returns a `bool` not an `option`. So what if the next function also returns an\n`option`? Well you'd get nested options, which doesn't really help anyone. So, instead, we would\nuse `bind`.\n\nNow let's replace the last item of one list with the last item of another. Note that both `last` and\n`init` return `option`:\n\n```Reason\nlet swapLast = (xs, ys) =\u003e\n  Option.(bind(RList.last(xs), ((x) =\u003e fmap(RList.append(x), RList.init(ys)))) |\u003e default(ys));\n\nswapLast([1,2,3], [4,5,6]); /* [4,5,3] */\nswapLast([], [4,5,6]); /* [4,5,6] */\n\n/* Or, with infix operators */\nopen Option.Infix;\n\nlet swapLast = (xs, ys) =\u003e\n  RList.last(xs) \u003e\u003e= ((x) =\u003e RList.init(ys) \u003c$\u003e RList.append(x)) |\u003e Option.default(ys);\n```\n\nHere we used `bind` and its infix variation `\u003e\u003e=` to apply a function that also returned an `option`.\n\n#### Applicatives\n\nLet's try checking if the last elements of two lists are equal. We could accomplish this using `bind`,\nbut that can be a little awkward.\n\n```Reason\nlet lastEqual = (xs, ys) =\u003e\n  Option.(apply(apply(Some(Util.eq), RList.last(xs), RList.last(ys))) |\u003e default(false));\n\nlastEqual([1,2,3], [4,5,3]); /* true */\nlastEqual([1,2,3], [4,5,6]); /* false */\nlastEqual([], [4,5,6]); /* false */\nlastEqual([1,2,3], []); /* false */\n\n/* Or, with infix operators */\nopen Option.Infix;\n\nlet lastEqual = (xs, ys) =\u003e\n  Some(Util.eq) \u003c*\u003e RList.last(xs) \u003c*\u003e RList.last(ys) |\u003e Option.default(false);\n```\n\n### Alternative\n\nWith alternative you can implement simple but powerful fallback mechanism your ADT-s;\n\nExample:\n\n```Reason\n  open Option;\n  let someData = some(\"Hello\");\n  let guard = fun\n  | true =\u003e pure() \n  | _ =\u003e empty();\n\n\n  let startWith: (string, string) =\u003e option(unit) = (str, char) =\u003e guard(Js.String.startsWith(char, str))\n  let dataWithFallback =\n    someData \n      \u003e\u003e= val_ =\u003e startWith(val_, \"T\")\n      \u003e\u003e= (_ =\u003e {Js.log(val_); some(Js.String.toUpperCase(val_))})\n      \u003c|\u003e some(\"Not started with T\")\n```\n\n### Translating JS Idioms\n\n#### Or chains\n\nTake the following example in Javascript:\n\n```Javascript\nlet x = a || b || c || d;\n```\n\nWe can't translate that directly to Reason, because there is no `null` or `undefined` in Reason.\nThe closest approximation would be `option`, in which we can string together `Some` and `None`\nto get the first one that is `Some`.\n\nThere is a helper function called `firstSome` and its infix variation `|?` that do exactly this.\n\n```Reason\n/* a, b, and c are all options, but d is not */\nlet x = a |? b |? c |\u003e default(d);\n```\n\nReference\n--------------------------------------------------------------------------------\n\n### Infix Operators\n\n- `\u003e\u003e=`: Monadic Bind\n- `\u003c$\u003e`: Functor Fmap\n- `\u003c*\u003e`: Applicative Apply\n- `\u003c||`: Point-free Function Compose\n- `||\u003e`: Point-free Function Pipe\n- `-\u003c\u003c`: Lens Compose\n- `\u003e\u003e-`: Lens Pipe\n- `|?`: Optional Or\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjonlaing%2Frationale","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjonlaing%2Frationale","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjonlaing%2Frationale/lists"}