asynkron / protoactor-dotnet

Proto Actor - Ultra fast distributed actors for Go, C# and Java/Kotlin
http://proto.actor
Apache License 2.0
1.71k stars 285 forks source link

F# API #217

Closed tjaskula closed 4 years ago

tjaskula commented 7 years ago

Hi, I wonder if contributing to an F# API is something you might be interested in ? Before starting anything I wanted to have a feedback.

cpx86 commented 7 years ago

Absolutely, that would be interesting :) I'm not very experienced with F# yet, so I can't really give any opinions right now design-wise, except that I think it should be built as an extension library and shouldn't introduce anything special in the core libraries.

rogeralsing commented 7 years ago

Agree with @cpx86 . The main issue I've seen so far with F# and Proto.Actor is the lack of decent ProtoBuf support in F#. You can use Protobuf libs with mutable types in F#, but it seems to be an issue to map F# special types such as records or DU's to protobuf.

If that is possible using Froto, or possible some other way, that would be awesome

tjaskula commented 7 years ago

Thanks for your feedback. Definietly, I take a note on extension library, that would be an add-on to what already exists and on serialization with protobuf.

I'll try to spike something and keep you posted.

tjaskula commented 7 years ago

I've been doing some investigation testing different approaches for protobuf serialization and F#. Unfortunately there are no libraries that support DU's serialization at all and speaking to the community there are no plans to support it. Unless some unexpected contribution which for now is unlikely to happen. Froto isn't maintained since months and it doesn't support DUs either. The work to add them since huge.

I've managed to serialize and deserialize simple DU with Mark Gravell library protobuf-net annotating F# types with attributes and with some hacky code, but I'm not sure what are the limits with this approach I can run into in the feature. Without speaking about supporting .proto files and code generation.

So I'm not sure, where to go from that. Would be a protobuf-net instead Google.protobuf library be a valid choice for F# Api? Of course with a limited support to the attribute annotations.

p69 commented 7 years ago

Hi, I've tried Proto.Actor for my pet f# project and experimented with some more idiomatic API. Now I have something like this:

let helloWorldActor =
    actorOf ( fun ctx->
      ctx.Message
      >>| fun(msg:Started) -> printfn "Hello World!"
    ) |> spawn

It's almost the same as Actor.FromFunc(..) but with infix operator (>>|) in actor body. It just help to match message to specific type. More complex example:

type HttpAction =
    | GET of string
    | POST of uri:string*content:string

type HttpActionResult =
    | Ok of uri:string
    | Error of uri:string*error:string

let doRequest (pid:PID) action =
    async {
        let client = new HttpClient()
        let (task, uri) =
            match action with
            | GET uri -> (client.GetAsync(uri), uri)
            | POST (uri, content) ->
                let postBody = new FormUrlEncodedContent(dict["key", content])
                (client.PostAsync(uri, postBody), uri)
        printfn "about starting download %s" uri
        try
            let! result = task |> Async.AwaitTask
            pid <! Ok(uri)
        with
        | exn as ex -> pid <! Error(uri, ex.ToString())
        return ()
    }
let httpActor =
    actor {
      receive (fun ctx ->
        ctx.Message
        >>| fun (msg:HttpAction) ->
                msg |> doRequest ctx.Sender |> Async.RunSynchronously
      )
    }
let urls = ["https://github.com/";"https://docs.microsoft.com";"https://www.tut.by"]
let mutable downloadActors = Map.empty

let mainActor =
    actor {
        receive (
          fun ctx ->
            ctx.Message
            >>= (fun (_:Started) ->
                for url in urls do
                  let child = httpActor |> spawnFromContext ctx
                  downloadActors <- downloadActors |> Map.add url child
                  child <? (GET(url), ctx.Self)
            )
            >>| (fun (msg:HttpActionResult)->
                match msg with
                | Ok uri ->
                    printfn "ok %s" uri
                    downloadActors.[uri] <! Stop.Instance
                | Error (uri, error) -> printfn "error %s %s" error uri
            )
        )
    } |> spawnNamed "main"

Here I created mainActor for making some http requests. There is some computation expression actor{..} instead of function actorOf. It's almost the same. Here we have one more infix operator (>>=) for matching message to specific type, but with piping message further as result of function. It helps compose logic with a lot of branches. Also we have (<!) Tell-operator and (<?) Request-operator. The same as in FSharpAPI for Akka.net. Also there is (<??) RequestAsync-operator. Configurations in computation expression:

let helloWorldActor =
    actor {
        //takes IContext->unit function
        receive (fun ctx->
          ctx.Message
          >>| fun(msg:Started) -> printfn "Hello World!")
        //takes unit->IMailbox function
        mailbox (fun()->Mailbox.UnboundedMailbox.Create())
        //takes IDispatcher
        dispatcher Mailbox.Dispatchers.DefaultDispatcher
        //takes ISupervisorStrategy
        supervisorStrategy Supervision.DefaultStrategy
        //takes list of IContext->unit functions
        receiveMiddlewares [
            fun ctx-> printfn "middleware 1";
            fun ctx-> printfn "middleware 2"
        ]
        //takes list of ISenderContext->PID->MessageEnvelope->unit functions
        sendMiddlewares [
            fun ctx pid envelope -> printfn "sender middleware 1";
            fun ctx pid envelope -> printfn "sender middleware 2"
        ]
    }

Here we make some basic configuration, including middlewares. Also there are additional functions for props configuration such as let withDispatcher d (props:Props) = props.WithDispatcher d . They are copy computation expression members, but good for using with (|>)-operator. The most tricky part was behaviors. To have possibilities switching between behaviors I made them as mutually recursive functions. Here is example:

let getBehaviors (behavior:BehaviorSwitcher) =
    let rec on (ctx:IContext) =
        ctx.Message
        >>| (fun (msg:BulbMessage)->
            match msg with
            | On-> printfn "It's already turned on!"
            | Off->
                printfn "Turning it off."
                behavior.become off
        )
    and off (ctx:IContext) =
        ctx.Message
        >>| (fun (msg:BulbMessage)->
            match msg with
            | On->
                printfn "Turning it on."
                behavior.become on
            | Off-> printfn "It's already turned off!"
        )
    [off;on]
let bulbActor =
    actor {
        behaviors getBehaviors
        receive (fun ctx ->
            ctx.Message
            >>= fun (_:Started) -> printfn "Bulb actor started. State is off!"
            >>| fun (msg:BulbMessage) ->
                    match msg with
                    | On ->  printfn "Turned on!"
                    | Off -> printfn "Turned off!"
        )
    } |> spawn

So in actor-computation we need to call behaviors function which is takes BehaviorSwitcher->MessagesHandler list. MessagesHandler is simple IContext->unit. BehaviorSwitcher is record with basic behavior operations:

type BehaviorSwitcher = {
  become:(IContext->unit)->unit
  becomeStacked:(IContext->unit)->unit
  unbecomeStacked:unit->unit
}

So, to setup behaviors we need a function which takes BehaviorSwitcher and returns list of handlers, which are know each other as they are mutually recursive functions. Also there is equivalent function for creating actor with behaviors

let behaviorActor = behaviorOf (fun (behavior:BehaviorSwitcher) ->
    let rec on (ctx:IContext) =
        ctx.Message
        >>| (fun (msg:BulbMessage)->
            match msg with
            | On-> printfn "It's already turned on!"
            | Off->
                printfn "Turning it off."
                behavior.become off
        )
    and off (ctx:IContext) =
        ctx.Message
        >>| (fun (msg:BulbMessage)->
            match msg with
            | On->
                printfn "Turning it on."
                behavior.become on
            | Off-> printfn "It's already turned off!"
        )
    [off;on]
)

I didn't add spawner configuration, because I'm not sure how useful it is (I'm still investigating this framework). Also there are no builders and helpers for persistence and remoting, because I didn't touch it yet. So what do you think? If you are interested in it then I can make PR.

tjaskula commented 7 years ago

@p69 Nice, but does this play with protobuf and DU/records?

sqeezy commented 6 years ago

I would be really interested in this and would test the F# Api if help is needed. Im not as firm with F# to implement it myself at the moment.

@p69 Could you publish logic you wrote for others to play around with?

irium commented 6 years ago

Recently I've found interesting project: https://github.com/mastoj/protoactor-fsharp.git

mastoj commented 6 years ago

As mentioned above. I put together my first stab at an F# api a while back. Didn’t read through this thread before I implemented it.

Regarding the protobuf issue I don’t see that as problem that should be solved in the F# api, that is a problem that I think we should keep outside the api imo. Internally, when running on the same machine it seems like DUs works just fine.

I would be nice if there were a good protobuf serializer that supported DUs for 100% F# scenarios, but as soon as you interact with any other language I think the DUs should be avoided.

cpx86 commented 6 years ago

Regarding serialization, we don't need to be sticklers for using Protobuf all the way. It's a good default since it's both performant and platform-agnostic, but if you don't intend to run a polyglot solution, then plugging in a native serializer (e.g Wire for .NET) is perfectly fine. For building F#-only solutions, perhaps a FsPickler plugin could be a good solution?

rogeralsing commented 4 years ago

Outdated, F# API will have to live in Contrib

mastoj commented 4 years ago

Fair enough

On Sun, 6 Sep 2020 at 11:41, Roger Johansson notifications@github.com wrote:

Outdated, F# API will have to live in Contrib

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/AsynkronIT/protoactor-dotnet/issues/217#issuecomment-687740490, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAD2NMABDGOVKFDHOQ7U77DSENKLJANCNFSM4DKGNL2A .