{"id":13879661,"url":"https://github.com/digital-fabric/affect","last_synced_at":"2025-04-23T14:05:06.094Z","repository":{"id":46313195,"uuid":"197712974","full_name":"digital-fabric/affect","owner":"digital-fabric","description":"Algebraic effects for Ruby","archived":false,"fork":false,"pushed_at":"2021-10-31T11:36:10.000Z","size":24,"stargazers_count":79,"open_issues_count":0,"forks_count":5,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-14T18:19:42.111Z","etag":null,"topics":["algebraic-effects","functional-programming","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","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/digital-fabric.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}},"created_at":"2019-07-19T06:14:07.000Z","updated_at":"2024-07-09T09:50:37.000Z","dependencies_parsed_at":"2022-09-05T10:40:45.424Z","dependency_job_id":null,"html_url":"https://github.com/digital-fabric/affect","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/digital-fabric%2Faffect","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/digital-fabric%2Faffect/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/digital-fabric%2Faffect/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/digital-fabric%2Faffect/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/digital-fabric","download_url":"https://codeload.github.com/digital-fabric/affect/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250447866,"owners_count":21432163,"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":["algebraic-effects","functional-programming","ruby"],"created_at":"2024-08-06T08:02:28.115Z","updated_at":"2025-04-23T14:05:06.023Z","avatar_url":"https://github.com/digital-fabric.png","language":"Ruby","readme":"# Affect - algebraic effects for Ruby\n\n[INSTALL](#installing-affect) |\n[TUTORIAL](#getting-started) |\n[EXAMPLES](examples) |\n\n\u003e Affect | əˈfɛkt | verb [with object] have an effect on; make a difference to.\n\n## What is Affect\n\nAffect is a tiny Ruby gem providing a way to isolate and handle side-effects in\nfunctional programs. Affect implements algebraic effects in Ruby, but can also\nbe used to implement patterns that are orthogonal to object-oriented\nprogramming, such as inversion of control and dependency injection.\n\nIn addition, Affect includes an alternative implementation of algebraic effects\nusing Ruby fibers, as well as an implementation of delimited continuations using\n`callcc` (currently deprecated).\n\n\u003e **Note**: Affect does not pretend to be a *complete, theoretically correct*\n\u003e implementation of algebraic effects. Affect concentrates on the idea of \n\u003e [effect contexts](#the-effect-context). It does not deal with continuations,\n\u003e asynchrony, or any other concurrency constructs.\n\n## Installing Affect\n\n```ruby\n# In your Gemfile\ngem 'affect'\n```\n\nOr install it manually, you know the drill.\n\n## Getting Started\n\nAlgebraic effects introduces the concept of effect handlers, little pieces of\ncode that are provided by the caller, and invoked by the callee using a uniform\ninterface. An example of algebraic effects might be logging. Normally, if we\nwanted to log a certain message to `STDOUT` or to a file, we would do the\nfollowing:\n\n```ruby\ndef mul(x, y)\n  # assume LOG is a global logger object\n  LOG.info(\"called with #{x}, #{y}\")\n  x * y\nend\n\nputs \"Result: #{ mul(2, 3) }\"\n```\n\nThe act of logging is a side-effect of our computation. We need to have a global\n`LOG` object, and we cannot test the functioning of the `mul` method in\nisolation. What if we wanted to be able to plug-in a custom logger, or intercept\ncalls to the logger?\n\nAffect provides a solution for such problems by implementing a uniform, \ncomposable interface for isolating and handling side effects:\n\n```ruby\nrequire 'affect'\n\ndef mul(x, y)\n  # assume LOG is a global logger object\n  Affect.perform :log, \"called with #{x}, #{y}\"\n  x * y\nend\n\nAffect.capture(\n  log: { |message| puts \"#{Time.now} #{message} (this is a log message)\" }\n) {\n  puts \"Result: #{ mul(2, 3) }\"\n```\n\nIn the example above, we replace the call to `LOG.info` with the performance of\nan *intent* to log a message. When the intent is passed to `Affect`, the\ncorresponding handler is called in order to perform the effect.\n\nIn essence, by separating the performance of side effects into effect intents,\nand effect handlers, we have separated the what from the how. The `mul` method\nis no longer concerned with how to log the message it needs to log. There's no \nhardbaked reference to a `LOG` object, and no logging API to follow. Instead,\nthe *intent* to log a message is passed on to Affect, which in turn runs the\ncorrect handler that actually does the logging.\n\n## The effect context\n\nIn Affect, effects are performed and handled using an *effect context*. The \neffect context has one or more effect handlers, and is then used to run code\nthat performs effects, handling effect intents by routing them to the correct\nhandler.\n\nEffect contexts are defined using either `Affect()` or the shorthand\n`Affect.capture`:\n\n```ruby\nctx = Affect(log: -\u003e msg { log_msg(msg) })\nctx.capture { do_something }\n\n# or\nAffect.capture(log: -\u003e msg { log_msg(msg) }) { do_something }\n```\n\nThe `Affect.capture` method can be called in different manners:\n\n```ruby\nAffect.capture(handler_hash) { body }\nAffect.capture(handler_proc) { body }\nAffect.capture(body, handler_hash)\nAffect.capture(body, handler_proc)\n```\n\n... where `body` is the code to be executed, `handler_hash` is a hash of effect\nhandling procs, and `handler_proc` is a default effect handling proc.\n\n### Nested effect contexts\n\nEffect contexts can be nested. When an effect context does not know how to\nhandle a certain effect intent, it passes it on to the parent effect context.\nIf no handler has been found for the effect intent, an error is raised:\n\n```ruby\n# First effect context\nAffect.capture(log: -\u003e(msg) { LOG.info(msg) }) {\n  Affect.perform :log, 'starting'\n  # Second effect context\n  Affect.capture(log: -\u003e(msg) { }) {\n    Affect.perform :log, 'this message will not be logged'\n  }\n  Affect.perform :log, 'stopping'\n\n  Affect.perform :foo # raises an error, as no handler is given for :foo\n}\n```\n\n\n## Effect handlers\n\nEffect handlers map different effects to a proc or a callable object. When an\neffect is performed, Affect will try to find the relevant effect handler by\nlooking at its *signature* (given as the first argument), and then matching\nfirst by value, then by class. Thus, the effect signature can be either a value,\nor a class (normally used when creating intent classes).\n\nThe simplest, most idiomatic way to define effect handlers is to use symbols as\neffect signatures:\n\n```ruby\nAffect(log: -\u003e msg { ... }, ask: -\u003e { ... })\n```\n\nA catch-all handler can be defined by calling `Affect()` with a block:\n\n```ruby\nAffect do |eff, *args|\n  case eff\n  when :log\n    ...\n  when :ask\n    ...\n  end\nend\n```\n\nNote that when using a catch-all handler, no error will be raised for unhandled\neffects.\n\n## Performing side effects\n\nSide effects are performed by calling `Affect.perform` or simply\n`Affect.\u003cintent\u003e` along with one or more parameters:\n\n```ruby\nAffect.perform :foo\n\n# or:\nAffect.foo\n```\n\nAny parameters will be passed along to the effect handler:\n\n```ruby\nAffect.perform :log, 'my message'\n```\n\nEffects intents can be represented using any Ruby object, but in a relatively\ncomplex application might best be represented using classes or structs:\n\n```ruby\nLogIntent = Struct.new(:msg)\n\nAffect.perform LogIntent.new('my message')\n```\n\nWhen using symbols as effect signatures, Affect provides a shorthand way to \nperform effects by calling methods directly on the `Affect` module:\n\n```ruby\nAffect.log('my message')\n```\n\n## Other uses\n\nIn addition to isolating side-effects, Affect can be used for other purposes:\n\n### Dependency injection\n\nAffect can also be used for dependency injection. Dependencies can be injected\nby providing effect handlers:\n\n```ruby\nAffect.on(:db) {\n  get_db_connection\n}.() {\n  process_users(Affect.db.query('select * from users'))\n}\n```\n\nThis is especially useful for testing purposes as described below:\n\n### Testing\n\nOne particular benefit of using Affect is the way it facilitates testing. When\nmutable state and side-effects are pulled out of methods and into effect\nhandlers, testing becomes much easier. Side effects can be mocked or tested\nin isolation, and dependencies provided through effect handlers can also be\nmocked. The following section includes an example of testing with algebraic\neffects.\n\n## Writing applications using algebraic effects\n\nAlgebraic effects have yet to be adopted by any widely used programming\nlanguage, and they remain a largely theoretical subject in computer science.\nTheir advantages are still to be proven in actual usage. We might discover that\nthey're completely inadequate as a solution for managing side-effects, or we\nmight discover new techniques to be used in conjunction with algebraic effects.\n\nOne important principle to keep in mind is that in order to make the best of\nalgebraic effects, effect handlers need to be pushed to the outside of your\ncode. In most cases, the effect context will be defined in the entry-point of\nyour program, rather than somewhere on the inside.\n\nImagine a program that counts the occurences of a user-defined pattern in a\ngiven text file:\n\n```ruby\nrequire 'affect'\n\ndef pattern_count(pattern)\n  total_count = 0\n  found_count = 0\n  while (line = Affect.gets)\n    total_count += 1\n    found_count += 1 if line =~ pattern\n  end\n  Affect.log \"found #{found_count} occurrences in #{total_count} lines\"\n  found_count\nend\n\nAffect(\n  gets: -\u003e { Kernel.gets },\n  log: -\u003e { |msg| STDERR \u003c\u003c \"#{Time.now} #{msg}\" }\n).capture {\n  pattern = /#{ARGV[0]}/\n  count = pattern_count(pattern)\n  puts count\n}\n```\n\nIn the above example, the `pattern_count` method, which does the \"hard work\",\ncommunicates with the outside world through Affect in order to:\n\n- read a line after line from some input stream\n- log an informational message\n\nNote that `pattern_count` does *not* deal directly with I/O. It does so\nexclusively through Affect. Testing the method would be much simpler:\n\n```ruby\nrequire 'minitest'\nrequire 'affect'\n\nclass PatternCountTest \u003c Minitest::Test\n  def test_correct_count\n    text = StringIO.new(\"foo\\nbar\")\n\n    Affect(\n      gets: -\u003e { text.gets },\n      log:  -\u003e |msg| {} # ignore\n    .capture {\n      count = pattern_count(/foo/)\n      assert_equal(1, count)\n    }\n  end\nend\n```\n\n## Contributing\n\nAffect is a very small library designed to do very little. If you find it\ncompelling, have encountered any problems using it, or have any suggestions for\nimprovements, please feel free to contribute issues or pull requests.\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdigital-fabric%2Faffect","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdigital-fabric%2Faffect","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdigital-fabric%2Faffect/lists"}