The original post contained a problem, I managed to solve, introducing a lot of issues with shared mutable state. Now, I'm wondering, if it can be done in a pure functional way.
Requests can be processed in a certain order.
For each order i
there is an effectiveness E(i)
Processing request should follow three conditions
There should be no delay between acquiring the first request and processing it
There should be no delay between processing some request and processing next request
When there are several orders of processing requests, the one with highest effectiveness should be chosen
Concrete example:
For an infinite list of integers, print them, so, that prime numbers are generally earlier, than not prime numbers
Effectiveness of ordering is reverse to the number of times we had primes in queue, but printed non prime
My first solution in C#
(not for primes, obviously) used some classes having a shared mutable state represented by a concurrent priority queue. It was ugly, because I had to manually subscribe classes to events and unsubscribe them, check that queue is not exhausted by one intermediate consumer before other consumer processes it and etc.
To refactor it, I chose Reactive Extensions library, which seemed to address issues with state. I understood that in the following case I couldn't use it:
The source
function accepts nothing and returns IObservable<Request>
The process
function accepts IObservable<Request>
and returns nothing
I have to write a reorder
function, which reorders requests on their way from source
to process
.
Internally reorder
has a ConcurrentPriorityQueue
of orders. It should handle two scenarios:
When
process
is busy with processingreorder
finds better orderings and updates the queueWhen
process
has requested a new orderreorder
returns the first element from queue
The problem was that if reorder
returned IObservable<Request>
, it wass unaware, whether items were requested from it, or no.
If reorder
had called OnNext
immediately upon receiving, it did not reorder anything and violated condition 3.
If it ensured, that it had found the best ordering, it violated conditions 1&2 because process
could become idle.
If reorder
returned ISubject<Request>
, it exposed an option to call OnError
and OnCompleted
to consumer.
If reorder
has returned the queue, I would have returned to where I started
The problem was that cold IObservable.Create was not lazy enough. It started exhausting queue with all requests when a subscription to it was made but results of only the first ones were used.
The solution I came up with is to return observable of requests, i.e. IObservable<Func<Task<int>>>
instead of IObservable<int>
It works when there is only one subscriber, but if there are more requests used, than there are numbers generated by source, they will be awaited forever.
This issue can probably be solved by introducing caching, but then consumer which consumed a queue fast will have side effects on all other consumers, because he will freeze the queue in less effective ordering, than it would be after some waiting.
So, I will post solution to the original question, but It's not really a valuable answer, because it introduces a lot of problems.
This demonstrates why doesn't functional reactive programming and side effects mix well. On the other hand, it seems I now have an example of a practical problem impossible to solve in pure functional way. Or don't I? If Order
function accepted optimizationLevel
as a parameter it would be pure. Can we somehow implicitly convert time to optimizationLevel
to make this pure as well?
I'd like to see such solution very much. In C#
or any other language.
Problematic solution. Uses ConcurrentPriorityQueue from this repo.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Reactive.Linq;
using DataStructures;
using System.Threading;
namespace LazyObservable
{
class Program
{
/// <summary>
/// Compares tuple by second element, then by first in reverse
/// </summary>
class PriorityComparer<TElement, TPriority> : IComparer<Tuple<TElement, TPriority>>
where TPriority : IComparable<TPriority>
{
Func<TElement, TElement, int> fallbackComparer;
public PriorityComparer(IComparer<TElement> comparer=null)
{
if (comparer != null)
{
fallbackComparer = comparer.Compare;
}
else if (typeof(IComparable<TElement>).IsAssignableFrom(typeof(TElement))
|| typeof(IComparable).IsAssignableFrom(typeof(TElement)))
{
fallbackComparer = (a,b)=>-Comparer<TElement>.Default.Compare(a,b);
}
else
{
fallbackComparer = (_1,_2) => 0;
}
}
public int Compare(Tuple<TElement, TPriority> x, Tuple<TElement, TPriority> y)
{
if (x == null && y == null)
{
return 0;
}
if (x == null || y == null)
{
return x == null ? -1 : 1;
}
int res=x.Item2.CompareTo(y.Item2);
if (res == 0)
{
res = fallbackComparer(x.Item1,y.Item1);
}
return res;
}
};
const int N = 100;
static IObservable<int> Source()
{
return Observable.Interval(TimeSpan.FromMilliseconds(1))
.Select(x => (int)x)
.Where(x => x <= 100);
}
static bool IsPrime(int x)
{
if (x <= 1)
{
return false;
}
if (x == 2)
{
return true;
}
int limit = ((int)Math.Sqrt(x)) + 1;
for (int i = 2; i < limit; ++i)
{
if (x % i == 0)
{
return false;
}
}
return true;
}
static IObservable<Func<Task<int>>> Order(IObservable<int> numbers)
{
ConcurrentPriorityQueue<Tuple<int, int>> queue = new ConcurrentPriorityQueue<Tuple<int, int>>(new PriorityComparer<int, int>());
numbers.Subscribe(x =>
{
queue.Add(new Tuple<int, int>(x, 0));
});
numbers
.ForEachAsync(x=>
{
Console.WriteLine("Testing {0}", x);
if (IsPrime(x))
{
if (queue.Remove(new Tuple<int, int>(x, 0)))
{
Console.WriteLine("Accelerated {0}", x);
queue.Add(new Tuple<int, int>(x, 1));
}
}
});
Func<Task<int>> requestElement = async () =>
{
while (queue.Count == 0)
{
await Task.Delay(30);
}
return queue.Take().Item1;
};
return numbers.Select(_=>requestElement);
}
static void Process(IObservable<Func<Task<int>>> numbers)
{
numbers
.Subscribe(async x=>
{
await Task.Delay(1000);
Console.WriteLine(await x());
});
}
static void Main(string[] args)
{
Console.WriteLine("init");
Process(Order(Source()));
//Process(Source());
Console.WriteLine("called");
Console.ReadLine();
}
}
}