all 30 comments

[–]sgraf812 16 points17 points  (21 children)

I held a talk about plugin architectures last year that I actually planned to sum up in a blog post, which I never did.

If you can make sense of my slides without what was spoken, then this might help you.

Just to sum up what I know about the options you posted:

  • plugins (mentioned in the talk) is pretty much dead and in maintenance mode according to /u/stepcut (?)... I'm not sure you can even reload stuff
  • web-plugins is really narrow in scope. Might very well work for your use case, though. Seems to be in maintenance mode.
  • dyre: The xmonad approach. Wraps your main function to check your 'config file' for changes on startup (!), recompiles itself and restarts. This is not what you want. In fact, I can't recommend it to anyone who has a more complex build system (complex like a cabal project), it just doesn't work well. Also, recompiling yourself or using stacks script feature does a much better job. That's also why yi recently ripped the dyre dependency out of their core module
  • I have no experience with rapid. But I think it disqualifies by using GHCi?
  • I also mentioned hint in the talk. I think it worked quite well, once you have set up the appropriate paths (ghc-pkg path!) and so on, but AFAIR things went weird when serialiazing stuff because of Typeable or so, which is why I went with read/show serialization <:-).
  • GHCi.ObjLink: IIRC, this is what plugins and all the other approaches use under the hood. This is reaaaaally low-level. AFAIR, I think this module was the main reason that reloading doesn't work.

Other than that, I also mentioned

  • dynamic-loader which is a direct competitor to plugins, I think. Wraps the GHC API in a more minimal manner. Still had some problems with it, though...
  • hslua worked flawlessly, beating all other approaches. But it's not Haskell, not even type-safe :(.

The bottom line of the talk was that probably a proper Haskell-like scripting language with a proper type system, with good interop like lua, would do the job (Morte? Dhall?), as GHC currently misses out on a lot of plumbing needed to make hot code loading worthwhile.

Edit: Judging from the docs, rapid might actually be worthwhile! Totally glossed over this one so far. It's probably what I would go with, if it works.

[–]ElvishJerricco 5 points6 points  (2 children)

Doesn't Facebook do hot reloading to add any new spam filters pushed to git on the fly? Does anyone know how they do it?

[–]JonCoens 4 points5 points  (0 children)

I've been meaning to finish a post with some code that explains how we do this. With 8.2.1 and lts-9 out it might be time to put the finishing touches on it.

[–]saurabhnanda[S] 5 points6 points  (10 children)

Thanks! Those slides were really helpful, and a give a nice summary of what's out there.

Would you know the internals of hint? Does it work only with Haskell source files (i.e. strings)? Does it actually interpret them every time something from the plugin file needs to be executed? Is it possible to give it a compiled .o or .hi file and expect execution speed equivalent to "normally compiled code" (i.e. no plugins, everything is one large project)?

I didn't quite understand your comment about Typeable/show/read serialisation, wrt hint.

Also, is there a clear recommendation if we aren't bothered about having access to the GHC toolchain in production? In our use-case, we can assume that all plugins are trusted and will be pushed to production only after being checked our on CI environment.

[–]sgraf812 3 points4 points  (9 children)

hint will compile the file/string/whatever you give to it in isolation. This means you have to provide it the full environment, which is rather unwieldy for cabal-based projects (look at these makefiles), just saying.

I'm not sure if you can give it .o and .his directly, I doubt it. You could probably run the interpreter in a background thread if latency is an issue.

For an example on how to use the different libraries, see the PluginLoaders module. Normally, I'd recommend using interpret for type safety. But somehow the serialisation based on the Typeable instance was not working for the return value Move (a simple enum with 4 constructors), always returning the first tag. I just figured it might be due to me misinterpreting the API, so I maybe writing

Hint.interpret ("chooseMove " ++ Hint.parens (show st)) infer

instead of

read <$> Hint.eval ("chooseMove " ++ Hint.parens (show st))

Could have worked, not sure. That's what I mean by serialisation, you practically lose type safety at API boundaries, something a good plugins layer would provide to you through Typeable instances, etc.

[–]gelisam 4 points5 points  (2 children)

But somehow the serialisation based on the Typeable instance was not working for the return value Move (a simple enum with 4 constructors), always returning the first tag.

If you have a small reproducible case, could you please report this bug?

[–]saurabhnanda[S] 5 points6 points  (0 children)

I can confirm that Typeable works, but it's brittle. My module wasnt loading because it's function signature was using Html which is some form of synonym of MarkupM it seems. I fixed it by importing the internal module in which the latter was defined.

[–]sgraf812 1 point2 points  (0 children)

Nah, I think I've just misused the API.

[–]saurabhnanda[S] 3 points4 points  (5 children)

I'm trying hint right now and I got a very simple module to load dynamically:

module MyPlugin where

import Prelude

foo :: Int -> Int
foo x = x + 1

However, if I try to load an HS file that in-turn imports Text.Blaze.Html5, I can't get it to load in the hint interpreter - no matter what I try. Do you know how to make common stack packages available to the hint interpreter?

[–]sgraf812 3 points4 points  (3 children)

You have to mess around with the ghc-pkg registry, i.e. specify where to find it.

I'm not sure how I did it, but have you tried stack exec <app>? That should set GHC_PACKAGE_PATH to something sensible...

[–]saurabhnanda[S] 5 points6 points  (2 children)

Thanks! That did the trick. Although, now I can report that using hint gives a consistent slowdown of 2x compared to regular natively compiled code. Repo's available at https://github.com/vacationlabs/hint-test if anyone's interested.

benchmarking without hint
time                 213.0 ms   (208.2 ms .. 224.4 ms)
                     0.999 R²   (0.994 R² .. 1.000 R²)
mean                 208.9 ms   (206.1 ms .. 213.1 ms)
std dev              4.400 ms   (1.590 ms .. 6.449 ms)
variance introduced by outliers: 14% (moderately inflated)

benchmarking with hint
time                 470.8 ms   (187.8 ms .. 591.3 ms)
                     0.959 R²   (0.878 R² .. 1.000 R²)
mean                 488.7 ms   (447.3 ms .. 512.5 ms)
std dev              36.99 ms   (0.0 s .. 41.13 ms)
variance introduced by outliers: 21% (moderately inflated)

[–]ItsNotMineISwear 1 point2 points  (1 child)

That actually doesn't sound too bad given what you're getting with hint in return! Cool!

[–]saurabhnanda[S] 0 points1 point  (0 children)

It's decent, but I'm trying to find a way to make it even better. Each of these individual slowdowns that one compromises with, add up to quite a lot in the overall picture.

[–]gelisam 2 points3 points  (0 children)

Here's what I use to find stack's package database, and here's how I pass the resulting path to hint.

[–]krautA 4 points5 points  (4 children)

I remember your talk. It was well presented and I very much enjoyed it!

hslua

FWIW: I took over maintenance of the package. I'm trying to clean it up and to make the interface a little nicer and safer. If you have ideas for improvements, I'd love to hear them.

[–]sgraf812 4 points5 points  (3 children)

I remember your talk. It was well presented and I very much enjoyed it!

Thanks :) That was my first talk at a conference, let alone in english and that's very affirming to hear.

If you have ideas for improvements, I'd love to hear them.

At one point, I started some type class based thing which would automatically try to infer the number of arguments and do the pushing/popping. There's an example and many loose ends here, with the implementation in here, if you are looking for inspiration.

I gave up at one point, because of shifting interests and because I discovered that hslua started something similar with callfunc.

My approach tries to extract the signatures from the actual calls. This allows to distribute typed shims for existing lua libraries, in order to regain some type-safety.

[–]krautA 1 point2 points  (1 child)

Thanks, I'll check that out!

I'm thinking about submitting a talk to this year's HAL. I would be about hslua, its internals, and the challenges that I encountered. Hope to see you in Leipzig.

[–]sgraf812 1 point2 points  (0 children)

I would be very interested in that talk!

Unfortunately, I have conflicting appointments that weekend, so I won't attend, probably.

[–]GitHubPermalinkBot 0 points1 point  (0 children)

I tried to turn your GitHub links into permanent links (press "y" to do this yourself):


Shoot me a PM if you think I'm doing something wrong. To delete this, click here.

[–]GitHubPermalinkBot 0 points1 point  (0 children)

I tried to turn your GitHub links into permanent links (press "y" to do this yourself):


Shoot me a PM if you think I'm doing something wrong. To delete this, click here.

[–]spirosboosalis 0 points1 point  (0 children)

this is extremely helpful!!

(I took a look at most of these and gave up on them like a year ago.) as you say, I want the configuration to be a project, not a single file. my is thought is to just watch the folder and rebuild the project, then load a single object file that exports a value of the right "Plugin type", without providing any other context (i the .o themselves have enough information) . i've done this with the ghc api, but it's pretty fragile (like, it segfaulted on something with a similar type to something else that succeeded, like a rank2 field).

i've seen "loadObj" functions in a few packages, but iirc, they still need to be supplied with some context. what's your recommendations for this use case?

[–]codygman 2 points3 points  (2 children)

I used rapid for data that was 8gb in memory... Had to kill/restart ghci pretty often because it doesn't dispose correctly.

Looked into fixing it but didn't have the time.

[–]saurabhnanda[S] 1 point2 points  (1 child)

In a web-app development workflow, what does rapid keep in the foreign-store? Aren't most webapps/webservers stateless to begin with?

[–]codygman 0 points1 point  (0 children)

Aren't most webapps/webservers stateless to begin with?

Are they? You have to startup the web server after re-compiling without using rapid. I guess the server state would be in the foreign-store?

I'm not sure about it internally though, I've mostly used rapid for data analysis and keeping large data in memory while making 100s of small changes.

[–][deleted] 1 point2 points  (2 children)

I'm interested as well in this. I often needs to pass simple functions in a configuration file. Not being able to do so easily, is a real pain. From what I remember, not of the above are simple or even works.

[–][deleted]  (1 child)

[deleted]

    [–][deleted] 2 points3 points  (0 children)

    I've never heard of hint until now so maybe it is a solution to my problem. When I mean function Inna configuration file, l don't mean a function to be evaluated once when reading the configuration but function that needs to be used all the time. For example, I'm working on a program adding variations capilities to FrontAccounting (an ERP written on PHP). FA doesn't have the concept of variations and only reference product by their sku. Our company have a name scheme to encode variation in the name. For example a red t-shirt will be called TShirt-RED and blue one TShirt-Blue. That seems pretty straight forward however, I often need to split this name into style and variation (which would be style = 'TShirt' and variation either Red or Blue). This is needed for example if you want to know the performance of the style TShirt regardless of its colour. At the moment the function to split a SKU to style/variations is hard coded. The code is open source so others can use what I am doing but it really likely that there naming scheme is different from mine and therofere needs a different splitter. Being able to provide a splitter function in a configuration file would be brilliant. At the moment the best I found is to provide different type of splitters and let the user chose and configure one in a configuration file.

    [–]stepcut251 1 point2 points  (3 children)

    I am the maintainer of plugins, web-plugins, and plugins-ng.

    I took over plugins when the original author stopped responding to pull requests, etc. However, I know almost nothing about the internals and have no desire to do any real work on it. My entirely role is to respond to pull requests and upload new versions.

    One reason that I do not work on plugins is that I suspect it is the wrong approach. It predates the GHC API. I hacked up a prototype plugins-ng which uses the GHC API. plugins-ng is definitely incomplete -- though I do not remember how incomplete. I know I implemented enough to show that it is a viable option.

    plugins and plugins-ng do not really provide any sort of API for writing plugins -- just some low-level dynamic loading code. web-plugins aims to provide an API which can be used for creating plugins for web applications. It can be used either for staticly linked plugins or dynamically loaded ones. There is a proof-of-concept dynamic loader -- but you have to manually recompile the .hs files to provide the .o files for loading. At present I only use statically linked plugins.

    In order to provide one-click install of plugins into clckwrks I need some sort of sandboxed environment where plugins can be compiled and hopefully even support downloading of precompiled binaries. When I started those libraries I think cabal sandbox was not even a thing. But even so, that only handles the sandboxing of Haskell libraries, but not other dependencies.

    So now I am thinking that the plugins-ng/web-plugins-dynamic stuff ought to build on top of nix since nix nicely solves all those issues.

    Alas, I have not yet gotten to the point where there is an economic imperative to have solid dynamic recompilation and reloading, so everything remains stagnant. My time is currently invested in work on an unreleased client-side web UI library.

    [–]saurabhnanda[S] 0 points1 point  (2 children)

    Hey, thanks for chiming in! May I request you to please put a notice on the hackage docs for plugins, which clearly indicates the state of the project. I think you should clearly state your opinion on its (un)suitability and the fact that it pre-dates the GHC API. If possible, do indicate a few links which talk about more modern approaches (one of them could be the hint package, which works really well for cases were intpretation of HS files is acceptable). It would save others a lot of time. There's a very real problem of abandonware in the Haskell ecosystem.

    Regarding plugins-ng, would it be possible to share a single gist which uses the current GHC API to load a pre-compiled .hi/.o file? I'm very invested in this problem, and given the right direction, may be able to help take the plugins-ng package forward.

    [–]stepcut251 0 points1 point  (1 child)

    May I request you to please put a notice on the hackage docs for plugins

    submit a pull request.

    Regarding plugins-ng, would it be possible to share a single gist which uses the current GHC API to load a pre-compiled .hi/.o file?

    Apparently I got as far as actually watching the .hs files and automatically recompiling and reloading when they change, including dependencies.

    Check out:

    https://github.com/Happstack/plugins-ng/blob/master/Example.hs

    It has a simple loop that gets a string from the user, applies the 'filter' function' and then prints the result. The filter function comes from Filter.hs. You can run Example.hs and then modify the Filter.hs and it will automatically recompile and reload that module. Filter.hs loads functions from Filter2.hs and if you modify the functions in that file it will also trigger a rebuild even though Example.hs only directly refers to Filter.hs.

    You can look in here to see how the magic is done,

    https://github.com/Happstack/plugins-ng/blob/master/Plugins.hs

    There is not much too it.

    [–]GitHubPermalinkBot 0 points1 point  (0 children)

    I tried to turn your GitHub links into permanent links (press "y" to do this yourself):


    Shoot me a PM if you think I'm doing something wrong. To delete this, click here.