In object-oriented programming an object is active when its state depends on clock. Usually an active object encapsulates a task that updates the object's state. To the outer world the object looks like a normal object with methods that can be called from outside. Implementation of such methods must have a certain synchronization mechanism with the encapsulated task in order to prevent object's state corruption.

A typical instance of an active object is an animation widget. The widget state changes with the time, while as an object it has all properties of a normal widget.

The task

Implement an active integrator object. The object has an input and output. The input can be set using the method Input. The input is a function of time. The output can be queried using the method Output. The object integrates its input over the time and the result becomes the object's output. So if the input is K(t) and the output is S, the object state S is changed to S + (K(t1) + K(t0)) * (t1 - t0) / 2, i.e. it integrates K using the trapeze method. Initially K is constant 0 and S is 0.

In order to test the object:

 set its input to sin (2π f t), where the frequency f=0.5Hz. The phase is irrelevant.
 wait 2s
 set the input to constant 0
 wait 0.5s
Verify that now the object's output is approximately 0 (the sine has the period of 2s). The accuracy of the result will depend on the OS scheduler time slicing and the accuracy of the clock.

require "math"
require "time"
 
# this enum allows us to specify what type of message the proc_chan received.
# this trivial example only has one action, but more enum members can be added
# to update the proc, or take other actions
enum Action
  Finished  # we've waited long enough, and are asking for our result
  # Update  # potential member representing an update to the integrator function
end
 
class Integrator
  property interval : Float64
  getter s : Float64 = 0f64
 
  # initialize our k function as a proc that takes a float and just returns 0
  getter k : Proc(Float64, Float64) = ->(t : Float64) { 0f64 }
 
  # channels used for communicating with the main fiber
  @proc_chan : Channel(Tuple(Action, Proc(Float64, Float64)|Nil))
  @result_chan : Channel(Float64)
 
  def initialize(@k, @proc_chan, @result_chan, @interval = 1e-4)
    # use a monotonic clock for accuracy
    start = Time.monotonic.total_seconds
    t0, k0 = 0f64, @k.call(0f64)
 
    loop do
      # this sleep returns control to the main fiber. if the main fiber hasn't finished sleeping,
      # control will be returned to this loop
      sleep interval.seconds
      # check the channel to see if the function has changed
      self.check_channel()
      t1 = Time.monotonic.total_seconds - start
      k1 = @k.call(t1)
      @s += (k1 + k0) * (t1 - t0) / 2.0
      t0, k0 = t1, k1
    end
  end
 
  # check the proc_chan for messages, update the integrator function or send the result as needed
  def check_channel
    select
    when message = @proc_chan.receive
      action, new_k = message
      case action
      when Action::Finished
        @result_chan.send @s
        @k = new_k unless new_k.nil?
      end
    else
      nil
    end
  end
end
 
# this channel allows us to update the integrator function,
# and inform the integrator to send the result over the result channel
proc_chan = Channel(Tuple(Action, Proc(Float64, Float64)|Nil)).new
 
# channel used to return the result from the integrator
result_chan = Channel(Float64).new
 
# run everything in a new top-level fiber to avoid shared memory issues.
# since the fiber immediately sleeps, control is returned to the main code.
# the main code then sleeps for two seconds, returning control to our state_clock fiber.
# when two seconds is up, this state_clock fiber will return control
# to the main code on the next `sleep interval.seconds`
spawn name: "state_clock" do
  ai = Integrator.new ->(t : Float64) { Math.sin(Math::PI * t) }, proc_chan, result_chan
end
 
sleep 2.seconds
proc_chan.send({Action::Finished, ->(t : Float64) { 0f64 }})
sleep 0.5.seconds
puts result_chan.receive