5

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));
    }
}
rysama
  • 1,674
  • 16
  • 28
  • I find F# more similar to Python than to C#. I would translate to Python first, make it tight in that language (because it is readable) and then go on. – Hamish Grubijan Dec 21 '09 at 21:33
  • 8
    I'm not familiar with F# but writing OO code to conceptualize functional code doesn't sound like a good idea to me. – pmr Dec 21 '09 at 21:34
  • Well, at the very least, I wrote it with FP in mind. The only OO I used was merely to get things to compile nicely. Anyone know how to put the code inside a scrollable region so it's not so large? – rysama Dec 21 '09 at 21:36
  • 2
    pmr: agreed. Brian's comment at http://stackoverflow.com/questions/1936471/f-design-pattern/1936619#1936619 "There is definitely a fundamental tension between whether you want to group things 'by type' or 'by operation'. The usual OO way is 'by type' whereas the FP (functional programming) way is 'by operation'" was a bit of a lightbulb moment for me! – itowlson Dec 21 '09 at 21:38
  • Looks like there is a bug in Pair.Run (when res.Next==null, you drop _two on the floor). Yes, this will be a million times shorter in F#, I'll give it a try shortly. – Brian Dec 21 '09 at 21:56
  • @Brian: Ah yes, thanks for spotting that. I'll fix it – rysama Dec 21 '09 at 22:05
  • 1
    Sidenode. Even this C# can be written better. – Dykam Dec 29 '09 at 09:52

4 Answers4

9

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.

Brian
  • 117,631
  • 17
  • 236
  • 300
  • Holy crap, that's so much smaller than the C#. Though it looks like you forgot to port "Sequence"? I'm excited to see what alternative solutions you can come up with – rysama Dec 21 '09 at 22:39
  • If you aren't constrained by the types of Runner/Result, then try using a list for the elements of the sequence and the match h::t syntax. – Guvante Dec 21 '09 at 22:47
  • (Yeah, I didn't bother with sequence, since it seemed the first four were enough to capure the 'essence' of the thing.) – Brian Dec 21 '09 at 23:24
  • Thanks Brian for the help and insight – rysama Dec 21 '09 at 23:33
5

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.

user229044
  • 232,980
  • 40
  • 330
  • 338
High Performance Mark
  • 77,191
  • 7
  • 105
  • 161
  • I do not disagree with what you are saying, but please note that even though my C# implementation uses objects, it is still very idiomatic to programming in a functional style. There is no mutation, the ctor is merely a way to pass arguments (or capture closures), and the "Run" method is basically just a high-order function. – rysama May 12 '10 at 00:53
1

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))
Guvante
  • 18,775
  • 1
  • 33
  • 64
  • 1
    Seems I overestimated the original posters personal ties to OO programming. Gotta say F# is definitely not built for it as you can see. – Guvante Dec 21 '09 at 22:48
  • The IRunner type was used just so that I could store things in collections and call the "Run" method. The "Result" type was just my cheezy way of doing a tuple. – rysama Dec 21 '09 at 22:51
  • @Guvante: to be fair, your F# code is still smaller than the original C# code I posted. – rysama Dec 21 '09 at 22:52
  • @RodYan: Yes, I was mainly saying that I assumed there was a deeper purpose beyond the obvious which was a mistake. At least I learned some more about F# syntax (Didn't know how interfaces worked) – Guvante Dec 21 '09 at 23:07
  • I actually think OO code is less cluttered in F# than C#, but in my own opinion you're basically writing C# with a little different syntax -- see this: http://stackoverflow.com/questions/1883246/none-pure-functional-code-smells/1884256#1884256 . – Juliet Dec 23 '09 at 00:21
  • @Juliet: It is true that there are much fewer {} which is nice. I was more comparing my version to Brian's, which are basically equivalent but mine is much longer. And this answer was written this way on purpose, I thought that binary compatibility was important :) – Guvante Dec 23 '09 at 16:23
1

You may consider using the built-in sequence support in F# (along with the very nice sequence expressions). This example is a little contrived but you could think of the program as a sequence of sequences, the "action queue" that Brian was hinting at. By adding the outer sequence it allows the inner sequence to fully control it's lifetime.

// an infinite sequence, repeat the last element of the underlying sequence
// now we can avoid using option types or null/boolean checks in our fold
let lastInfinite (a:seq<'a>) = seq {
    let last = ref Unchecked.defaultof<'a>
    for i in a do
        last := i
        yield i
    let last' = !last
    while true do
        yield last'
}

let addOne = seq [ fun x -> x + 1 ]
let idle = seq [ id<int> ]
let repeat run counter = Seq.truncate counter (lastInfinite run)
let sequence = Seq.concat
let pair one two = lastInfinite (Seq.append one two)

let program = seq [ repeat idle 5;
                    repeat addOne 100;
                    idle; ]

// use Seq.scan if you need lazy access to the intermediate results
// or perhaps Seq.map...
let result = Seq.concat program
             |> Seq.fold (fun state value -> value state) 0

printfn "result: %A" result
Nathan Howell
  • 4,627
  • 1
  • 22
  • 30