Haskell on deno?

It seems that there are now multiple “serverless” infrastructure services out there that run JS (including Wasm) in the deno runtime: Netlify Edge Functions, Deno Deploy, Supabase Edge Functions

I find this model more appealing that services where I have to build a full system container, and worry about provisioning (virtual) servers.

So naturally the next question is: Can I run Haskell on these?

With GHC gaining both JS and WebAssembly targets, it seems that there are possibly multiple ways to compile Haskell to something that can be run on these platforms, with probably just a small shim to be written. It remains to be seen if the output is small enough (Deno Deploy has a limit of 20MB), and if the startup time and memory consumption is acceptable.

4 Likes

FWIW I run the wasm backend’s output using deno on a daily basis. https://deno.land/std/wasi/snapshot_preview1.ts works pretty fine.

5 Likes

Regarding code size: One real-world example that I know is ormolu-live, https://ormolu-live.tweag.io/ormolu.f1c4a8ef.wasm is 18.32MB uncompressed. But it should be easy to upload a compressed bundle and add extra decompression logic, so I wouldn’t worry about code size here.

Regarding startup time and memory consumption: The wasm backend uses cross-compiled vanilla GHC RTS. You can tune the RTS flags and use different GC algorithms (including the nonmoving gc), and produce profiles as if they are native programs.

It’s even possible to link a wasm program first, run some Haskell computation to initialize some heap state, then snapshot the entire program state into a new wasm program, allowing it to hit the ground with zero initialization overhead in the servers. I shall write a tutorial in ghc-wasm-meta next week about this.

11 Likes

Cool! I guess the next question is how to provide the interface expected by these service providers, i.e. what is the Haskell equivalent of this Typescript code:

export default async (request: Request) => {
  return new Response("Hello, World!", {
    headers: { "content-type": "text/html" },
  });
};

(from Edge Functions API | Netlify Docs)

1 Like

That sounds amazing!

1 Like

For this particular use case you may export a Text -> IO Text Haskell function. The JavaScript glue takes care of marshaling request/response.

EDIT: the type signature is a conceptual one, the FFI mechanism currently doesn’t allow using Text directly as argument/return value. But you get the point, you can move blobs across JavaScript/Haskell.

2 Likes

It would be interesting to compare

  • Haskell compiled to JS on a deno-based platform
  • Haskell compiled to Wasm on a deno-based platform
  • Haskell compiled to static Linux binary on Amazon Lambda (this is what I am using)
  • Maybe Haskell compiled to JS/Wasm on Amazon Lambda?

and see what has the better latency (and whatever matters)

2 Likes

I think ormolu is huge because it embeds the information about Hackage operator fixities as code through TH. Turning that into runtime information would likely both fix how long it takes to compile and the size of the output.

2 Likes

Actually, we don’t/can’t do that via TH as the WASM backend does not support TH yet. Instead, we initialize the fixity DB (1.2 MB) at runtime, but it is not part of the 18.32 MB mentioned above.

See Pre-init Ormolu Live with Wizer by amesgen · Pull Request #991 · tweag/ormolu · GitHub for doing this initialization at compile time as mentioned by @TerrorJack above. In the current state, this increases the WASM sizes by at least 10 MB; but that is probably not the end of the story.

2 Likes

Are you referring to GitHub - bytecodealliance/wizer: The WebAssembly Pre-Initializer?

1 Like

Yes. It’s already used in current version of ormolu-live. The usage instruction is available at Glasgow Haskell Compiler / ghc-wasm-meta · GitLab.

2 Likes

Thanks, @TerrorJack, for the instructions there! I am however stuck trying to import something. Can you tell me what’s wrong here;

~/projekte/programming/haskell/haskell-on-fastly $ cat fastly-sys.s
.hidden http_resp_new
.globl http_resp_new
.import_module http_resp_new, fastly_http_resp
.import_name http_resp_new, new
.functype http_resp_new () -> (i32)

~/projekte/programming/haskell/haskell-on-fastly $ cat hello.hs
import Data.Word

foreign import ccall unsafe "http_resp_new" http_resp_new :: IO Word32


main = do
    putStrLn "hello world"
    http_resp_new >>= print

~/projekte/programming/haskell/haskell-on-fastly $ wasm32-wasi-ghc --make fastly-sys.s hello.hs
[2 of 2] Linking hello.wasm
wasm-ld: error: import module mismatch for symbol: http_resp_new
>>> defined as env in hello.o
>>> defined as fastly_http_resp in fastly-sys.o
clang-16: error: linker command failed with exit code 1 (use -v to see invocation)
wasm32-wasi-ghc-9.7.20230215: `clang' failed in phase `Linker'. (Exit code: 1)

~/projekte/programming/haskell/haskell-on-fastly $ wasm2wat fastly-sys.o |grep resp_new
  (import "fastly_http_resp" "new" (func $http_resp_new (type 0))))

~/projekte/programming/haskell/haskell-on-fastly $ wasm2wat hello.o |grep resp_new
  (import "env" "http_resp_new" (func (;5;) (type 0)))

1 Like

Ah, thanks for filing the bug, the assembly source example I gave was actually wrong! It confuses wasm-ld since in hello.o, the unresolved symbol http_resp_new is always an env import given how the current C toolchain works.

Here is a fastly-sys.c instead which can unblock your example:

#include <stdint.h>

uint32_t _http_resp_new(void) __attribute__((
  __import_module__("fastly_http_resp"),
  __import_name__(("new"))
));

uint32_t http_resp_new(void) {
  return _http_resp_new();
}

Note that the http_resp_new function being called by Haskell is merely a wrapper that calls the actual wasm import fastly_http_resp.new. See Attributes in Clang — Clang 17.0.0git documentation for more explanation of these clang-specific attributes to access wasm imports in C. I’ll make sure to update the documentation later this week.

1 Like

Thanks for the swift response!

So there is no way to import from fastly_http_resp in the Haskell FFI directly? I could imagine something like

foreign import ccall unsafe "fastly_http_resp new" http_resp_new :: IO Word32

or some other way to encode both module and function name in the import statement?

1 Like

Not yet. It’s surely doable, but it will involves some changes in GHC internals, to make it aware of distinction between a raw wasm import and an actual wasm function defined in other objects.

1 Like

Thanks. That unblocked me, and here we go: Haskell compiled to WebAssembly running on Fastly’s Edge Computing platform:

https://haskell-on-fastly.edgecompute.app/

More on Haskell via WebAssembly on Fastly

3 Likes

Did you choose fastly over the other platforms because it was easier to integrate wasm Haskell?

1 Like

I chose fastly because I was at a WebAssembly event, and some fastly people were there, so I was inspired to see if I can get it to work during some of the talks :slight_smile:

Depending on the workload I might still trust a static executable uploaded to AWS or similar more. Didn’t do any measurements, though.

3 Likes