I'm trying to write a small little scripting engine for a bullet hell game and I would like to do it in F#. I wrote some C# code to conceptualize it, but I'm having trouble porting it to F#. The C# code is posted below, and I would like some help porting it to F#. I have a feeling the matching F# code will be significantly smaller. I'm open to any sort of creative solutions :)
interface IRunner
{
Result Run(int data);
}
struct Result
{
public Result(int data, IRunner next)
{
Data = data;
Next = next;
}
public int Data;
public IRunner Next;
}
class AddOne : IRunner
{
public Result Run(int data)
{
return new Result(data + 1, null);
}
}
class Idle : IRunner
{
public Result Run(int data)
{
return new Result(data, null);
}
}
class Pair : IRunner
{
IRunner _one;
IRunner _two;
public Pair(IRunner one, IRunner two)
{
_one = one;
_two = two;
}
public Result Run(int data)
{
var res = _one.Run(data);
if (res.Next != null)
return new Result(res.Data, new Pair(res.Next, _two));
return new Result(res.Data, _two);
}
}
class Repeat : IRunner
{
int _counter;
IRunner _toRun;
public Repeat(IRunner toRun, int counter)
{
_toRun = toRun;
_counter = counter;
}
public Result Run(int data)
{
var res = _toRun.Run(data);
if (_counter > 1)
{
if (res.Next != null)
return new Result(res.Data,
new Pair(res.Next,
new Repeat(_toRun, _counter - 1)));
return new Result(res.Data, new Repeat(_toRun, _counter - 1));
}
return res;
}
}
class Sequence : IRunner
{
IEnumerator<IRunner> _runner;
public Sequence(IEnumerator<IRunner> runner)
{
_runner = runner;
}
public Result Run(int data)
{
var res = _runner.Current.Run(data);
bool next = _runner.MoveNext();
if (res.Next != null)
{
return new Result(res.Data,
new Pair(res.Next, new Sequence(_runner)));
}
return new Result(res.Data, new Sequence(_runner));
}
}
Here's something that's almost a direct translation of the same solution strategy.
That said, I think there may be a better/simpler representation choice, I'm still mulling it over.
type Runner = int -> Result
and Result = Result of int * option<Runner>
let AddOne = fun x -> Result(x+1, None)
let Idle = fun x -> Result(x, None)
let rec Pair(r1,r2) = fun x ->
match r1 x with
| Result(data,None) -> Result(data, Some(r2))
| Result(data,Some(next)) -> Result(data,Some(Pair(next,r2)))
let rec Repeat r n = fun x ->
if n = 0 then r x else
match r x with
| Result(data,None) -> Result(data, Some(Repeat r (n-1)))
| Result(data,Some(next)) -> Result(data, Some(Pair(next, Repeat r (n-1))))
EDIT
Here's another way that's a little more refined... am still trying to see if there's a good way to work in a "list", since the results seem isomorphic to cons cells...
type Runner = Runner of (int -> int * option<Runner>)
let AddOne = Runner(fun x -> x+1, None)
let Idle = Runner(fun x -> x, None)
let rec Pair(Runner(r1),R2) = Runner(fun x ->
match r1 x with
| data,None -> data, Some(R2)
| data,Some(next) -> data, Some(Pair(next,R2)))
let rec Repeat (Runner(r) as R) n = Runner(fun x ->
if n = 0 then r x else
match r x with
| data,None -> data, Some(Repeat R (n-1))
| data,Some(next) -> data, Some(Pair(next, Repeat R (n-1))))
EDIT
One more version, it uses lists, but now I've a feeling for what's weird here...
type Runner = Runner of (int -> int * list<Runner>)
let AddOne = Runner(fun x -> x+1, [])
let Idle = Runner(fun x -> x, [])
let rec Pair(Runner(r1),R2) = Runner(fun x ->
match r1 x with
| data,xs -> data, xs @ [R2]) // inefficient
let rec Repeat (Runner(r) as R) n = Runner(fun x ->
if n = 0 then r x else
match r x with
| data,xs -> data, xs @ List.init (n-1) (fun _ -> R)) // inefficient
It's almost just like an 'Action queue', a list of int->int
functions. But each guy can produce some 'suffix actions' that run immediately after him (but before the remaining work in the would-be queue), and trying to maintain the ordering with a purely functional data structure is potentially inefficient (without the right tree/queue library at hand). It would be interesting to know how this will be used/consumed, as perhaps a small change there might allow for a completely different strategy.
Forget the C#, go back to the design documents (or whatever) and re-implement. I mean, literally, forget the C#. The worst thing you can do in F# is to write C#. This is, of course, an instance of a generic rule: The worst thing you can do in language X is to write a program in language Y. Bind X and Y as you wish.
I am assuming that IRunner and Result are predefined, since if not you should redesign the system to be more focused on FP concepts without all this inheritance.
Anyway, here is a binary (I believe) counterpoint to the given example
type AddOne =
interface IRunner with
member this.Run(data) = new Result(data+1, null)
type Idle =
interface IRunner with
member this.Run(data) = new Result(data, null)
type Pair(one:IRunner, two:IRunner) =
interface IRunner with
member this.Run(data) =
let res = one.Run(data)
if res.Next <> null then
new Result(res.Data, new Pair(res.Next, two))
else
new Result(res.Data, two)
type Repeat(toRun:IRunner, counter:int) =
interface IRunner with
member this.Run(data) =
let res = toRun.Run(data)
if counter > 1 then
if res.Next <> null then
new Result(res.Data, new Pair(res.Next, new Repeat(toRun, counter - 1)))
else
new Result(res.Data, new Repeat(toRun, counter - 1))
else
res
type Sequence(runner:System.Collections.Generic.IEnumerator<IRunner>) =
interface IRunner with
member this.Run(data) =
let res = runner.Current.Run(data)
let next = runner.MoveNext()
if res.Next <> null then
new Result(res.Data, new Pair(res.Next, new Sequence(runner)))
else
new Result(res.Data, new Sequence(runner))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With