concur

Version, currently 1.0.04 versions

github.com/lbarasti/concur

A collection of concurrency utilities for Crystal, leveraging Fibers and channels.

18 stars
0 dependents
License: MIT

Nothing has been indexed for 1.0.0 yet. The tag is recorded, its shard.yml has not been read, so the manifest and dependency list below are empty because they are unknown rather than because they are absent.

Installation

# Add this to your shard.yml
dependencies:
  concur:
    github: lbarasti/concur
    version: ~> 1.0.0

Then run:

shards install

shard.yml

No shard.yml has been indexed for 1.0.0. You can read it on the repository.

Dependencies

Unknown: the shard.yml for this version has not been read yet.

README

This README is the one indexed from the repository at its latest ref, not from the tag for this version.

![GitHub release](https://img.shields.io/github/release/lbarasti/concur.svg)
![Build Status](https://github.com/lbarasti/concur/workflows/spec_and_docs/badge.svg)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg)](https://lbarasti.github.io/concur)

# concur

Concur is organised around two APIs:
* A `Future` API to deal with asynchronous computations
* An augmented `Channel` API with primitives to simplify event-driven programming.

## Installation

Add the dependency to your `shard.yml` and run `shards install`:

  ```yaml
  dependencies:
    concur:
      github: lbarasti/concur
  ```

## Usage

```crystal
require "concur"
```

### Using Future
You can use `Future` to wrap asynchronous computations that might fail.
```crystal
f = Future.new {
  sleep 2 # a placeholder for some expensive computation or for a lengthy IO operation
  "Success!"
}

f.await # => "Success!"
```
If you want to keep on manipulating the result of a future in a separate fiber, then you can rely on `Future`'s instance methods.

For example, given a future `f`, you can apply a function to the wrapped value with `#map`, filter it with `#select` and recover from failure with `#recover`

```crystal
f.map { |v| v.downcase }
  .select { |v| v.size < 3 }
  .recover { "default_key" }
```

Here is a contrived example to showcase some other common methods.

You can combine the result of two Futures into one with `#zip`:

```crystal
author_f : Future(User) = db.user_by_id(1)
reviewer_f : Future(User) = db.user_by_id(2)

permission_f : Future(Bool) = author_f.zip(reviewer_f) { |author, reviewer|
  author.has_reviewer?(reviewer) 
}
```

You can use `#flat_map` to avoid nesting futures:

```crystal
content_f : Future(Content) = permission_f
  .flat_map { |reviewer_is_allowed|
    if reviewer_is_allowed
      db.content_by_user(1) # => Future(Content)
    else
      raise NotAllowedError.new
    end
  }
```

And perform side effects with `#on_success` and `#on_error`.

```crystal
content_f
  .on_success { |content|
    reviewer_f.await!.email(content)
  }
  .on_error { |ex| log_error(ex) }
```

### Using Channel
Concur tries to increase the usability of `Channel` by adding a collection of transformation and callback methods to its API.

#### Sources: the beginning and the end
A Concur flow usually starts with a *source*.

```crystal
s1 = Concur.source(1..10) # returns a Channel(Int32) receiving each value of the range

s2 = Concur.source(initial_state: 0) { |state| (state + 1)**2 } # returns a Channel(Int32) receiving the values (i + 1)^2 for i = 0,1,...

s3 = Concur.every(1.second) { Time.utc } # returns a Channel(Time) receiving a timestamp approximately every second
```

Sources can also be built by simply defining a channel and having some fiber send values to it. For example, suppose you want your application to react to some keyboard event
that lets you register a callback:

```crystal
s4 = Channel(KeyboardEvent).new
keybord.on_keypress { |event| s4.send event }
```

Now that we have some sources, we can leverage Concur's API to materialise them or define some callbacks that will run every
time the channel receives a value.

```crystal
s1.listen { |v| puts v } # prints values from 1 to 10

s2.take(3) # => [1, 4, 25]

s3.each { |time|
  puts "you're living in the past" if time.year < 2022
}
```

#### Sources: the middle
We've seen how we can create a source and how we can *consume* it. Let's now look at how we can transform it.

Here is an example where we merge two event streams, batch them, process them into a new state and then persist that.

```crystal
keypress = Channel(KeyboardEvent).new
mouse_click = Channel(MouseEvent).new

# persist the state of the system every 5 events or every second,
# whichever happens first
keypress.merge(mouse_click)
  .batch(size: 5, interval: 1.seconds)
  .scan({"", {0,0}}) { |state, events|
    events.reduce(state) { |s, e|
      case e
      in MouseEvent
        {s[0], e.pos}
      in KeyboardEvent
        {s[0] + e.key, s[1]}
      end
    }
  }.each { |state| persist(state) }
```

Concur also lets you spin off concurrent computations very easily. Here is an example
where we let 4 workers compute the distance of a 2D point from the origin of a Cartesian plane.
We then produce [an estimate of the constant Pi](https://en.wikipedia.org/wiki/Approximations_of_%CF%80#Summing_a_circle's_area) and look at how many iterations it took the process
to generate 10 consecutive estimates with a relative error of less than `1e-5`.

```crystal
source(Random.new) { |gen| {gen, {gen.rand, gen.rand}} }
  .map(workers: 4) { |(x,y)| x**2 + y**2 }
  .scan({0, 0}) { |acc, v|
    v <= 1 ? {acc[0] + 1, acc[1]} : {acc[0], acc[1] + 1}
  }.map { |(inner, outer)| 4 * inner / (inner + outer)}
  .zip(source(1..)) { |estimate, iteration| {iteration, estimate} }
  .batch(10, 1.second)
  .select { |estimates|
    estimates.all? { |(i, e)|
      (e - Math::PI).abs / Math::PI < 1e-5
    }
  }.flat_map { |estimates| estimates.map(&.first) }
  .take 1 # => [235191]
```

Check out the [API docs](https://lbarasti.com/concur/) and the `/examples` folder for more details.

## Development

Run `shards install` to install the project dependencies. You can then run `crystal spec` to verify that all the tests are passing.

## Contributing

1. Fork it (<https://github.com/lbarasti/concur/fork>)
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request

## Contributors

- [lbarasti](https://github.com/lbarasti) - creator and maintainer