2

The task here is as follows :

  1. The client requests a function to be executed providing a name and it's arguments.
  2. The server executes the function with the provided arguments and returns the result.

Something like this is fairly easy to implement in a dynamic typed language like Python. But, given Haskell's static typing, this seems increasingly difficult.

Here is my initial stab at the problem :

  • Assume that the functions in the module only take arguments which are serializable. Implement a Serializable type class (idea taken from Cloud Haskell).

  • Store the functions in a map keyed using the function name. Doesn't work. The values of the map (function objects) need not be of same type.

  • The only thing I could come up with is to parse the module and generate a chain of if else statements (or a case statement) invoking the right function based on the input string and then serializing the result.

    This gives a pathetic performance as the worst case 'lookup' time of a function will be dependent on the number of functions in the module.

What is the right way to approach this problem? Is there something trivial that I'm missing here?

Community
  • 1
  • 1
Vamshi Surabhi
  • 437
  • 4
  • 15
  • If the only thing that bothers about the last option is the linear time lookup, you can write/generate a number of functions of type `Bytes -> Bytes`, one for each function f, of shape `let (arg1, ..., argN) = deSerializeArgs input in serialize $ f arg1 ... argN`. Those functions can then be put into a map or whatever other structure you like. Of course, this is still quite some work and rather ugly. –  Jan 25 '14 at 14:30
  • Is there a problem with using cloud-haskell (aka distributed-process)? – Thomas M. DuBuisson Jan 25 '14 at 19:23
  • You can use a `Map` for looking up the functions - e.g. `Map String Handler` where `Handler` is something like `Bytes -> Bytes` or `FileHandle -> IO ()`. You'll just have a write a wrapper for each RPC function. – ErikR Jan 25 '14 at 19:44
  • One man's handy run-functions-over-the-network is another man's security black hole. Be sure the remotely run arbitrary code model is the best basis for your project before you use it as the foundation stone. – not my job Jan 26 '14 at 00:31
  • Haskell is the best dynamically typed language out there ;) Wrap your functions in a `Dynamic -> Dynamic` typed wrapper (from `Data.Dynamic`). – n. m. could be an AI Jan 26 '14 at 12:57

2 Answers2

1

This is the solution I could come up with. The idea is from @delnan/@user4502 coupled with the Aeson library.

Let's say I would like to 'serverize' a module Foo. A new module Bar will be generated where there are wrappers for the functions in module Foo.

For example, if a function splitAt in module Foo has the type signature :

    splitAt :: Int -> [Int] -> ([Int], [Int])

The corresponding wrapper function _splitAt in Bar will have the type signature :

    _splitAt :: ByteString -> ByteString

The generated Bar module will be something like this :

    module Bar where

    import qualified Data.ByteString.Lazy as L
    import qualified Data.ByteString.Lazy.Char8 as LC
    import qualified Foo as F
    import qualified Data.Aeson as DA

    errMsg = LC.pack "Invalid Input"

    _splitAt :: L.ByteString->L.ByteString
    _splitAt input = let args = DA.decode input :: Maybe (Int, [Int])
                     in case args of 
                       (Just (arg1, arg2)) -> DA.encode $ F.splitAt arg1 arg2
                       Nothing -> errMsg

    -- And the rest of the functions

Now, since all functions are of the type ByteString -> ByteString, they can be placed in a Map.

This works but, can the code of _splitAt be refactored to avoid the type suggestion (not sure of the terminology) Maybe (Int, [Int])?

Vamshi Surabhi
  • 437
  • 4
  • 15
1

Let's assume that all our functions work within some common monad M, which I guess will be a common case (to handle errors etc.). So our functions will have types like f :: a -> b -> c -> M d.

The idea is to convert all functions into one common type. Let's say we're using aeson. Then the common type we're seeking is

type RPC m = forall i o . (FromJSON i, ToJSON o) => i -> m o

(using RankNTypes).

We can proceed as follows:

  • Given a function name, use TH's reify to inspect its type.
  • Enumerate how many arguments a function has and apply the corresponding uncurrying function. For example, our example above has 3 arguments so it'd be converted into

    uncurry3 f :: (a, b, c) -> M d
    

    where uncurry3 can be easily generated automatically using TH (perhaps there is a package for it). Don't forget to convert functions of type M a into () -> M a.

  • Now all functions fit into RPC M, so we can create a Map String (RPC M) from a list of functions (reify also gives us function names). For example something like mkRPC :: [Name] -> Q Exp.
  • Finally, we create a handler that takes Map String (RPC M), a user request and processes it, calling the appropriate function from the map.

The client side would be very similar.

Petr
  • 62,528
  • 13
  • 153
  • 317
  • I'm wondering if you would prefer TH over a type class approach -- e.g. [this post](http://stackoverflow.com/questions/21362498/applying-a-list-of-strings-to-an-arbitrary-function). Thanks! – ErikR Jan 26 '14 at 19:52
  • @user5402 I've been already using TH in the project, so it was a natural choice. A type-class approach would be nice, but I'm afraid it's not possible to avoid `UndecidableInstances` or `OverlappingInstances`, which usually makes the result useless. The general problem is that if you have a 1-argument function `a -> b`, you can specialize `b` to `c -> d` and suddenly you have a 2-argument function `a -> c - > d`. – Petr Jan 26 '14 at 20:23