Purescript-native can now target Golang

purescript-native now supports both Go and C++11 as intermediate languages.

The purescript-native “suite” comprises two utilites: pscpp and psgo. The two share transpiler code and a general design but function independently of one another. Foreign implementations are provided by either C++11 for pscpp or Go for psgo; please see the standard library implementations purescript-native-cpp-ffi and purescript-native-go-ffi for examples. Even if you’re not fluent in C++ or Go, the code should look familiar if you’ve dealt with the javascript FFI (especially the Go versions).

The Go backend has feature parity with the C++11 one but hasn’t been exercised as much. Both pass all of the relevant standard purescript compiler tests.

In terms of transcompilation targets, C++11 has the advantage of platform ubiquity (including things like the Arduino Uno) but Go is a simpler language, compiles quickly, has a built-in concurrent garbage collector, and has plenty of “batteries included” (such as reflection, Unicode strings and identifiers, and its own build system).

18 Likes

Would it be possible to see an example of calling Go from Purescript? I’m specifically wondering whether calling Go from Purescript is ‘fully’ typesafe and how much wrapper code has to be written.

2 Likes

It’s extremely similar to calling javascript functions using the default (js) backend, if you’re familiar with that. If not, some of these string functions should give you an idea.

3 Likes

Hi, I’m trying to test this out, and I’m running into an error.

[nix-shell:~/Projects/ps-go]$ cat src/Main.purs
module Main where

import Prelude

import Effect (Effect)
import Effect.Console (log)

main :: Effect Unit
main = do
  log "🍝"

Bootstrapped everything via nix:

$ cat default.nix
# This imports the nix package collection,
# so we can access the `pkgs` and `stdenv` variables
with import <nixpkgs> {};

# Make a new "derivation" that represents our shell
stdenv.mkDerivation {
  name = "purs-sandbox";

  # The packages in the `buildInputs` list will be added to the PATH in our shell
  buildInputs = [
    # see https://nixos.org/nixos/packages.html to search for more
    pkgs.nodejs-10_x
    pkgs.go
    pkgs.gcc
    pkgs.libpcap
  ];

  shellHook = ''
    export PATH="$PWD/node_modules/.bin/:$PWD/go/bin/:$PATH"
    export GOPATH="$PWD/go"
  '';
}

Tried to build:

[nix-shell:~/Projects/ps-go]$ spago build -- -g corefn && psgo
Installation complete.
Compiling Type.Data.RowList
Compiling Record.Unsafe
Compiling Type.Data.Row
Compiling Data.Symbol
...
Build succeeded.
/Users/me/Projects/ps-go/output/src/Data_Boolean/Data_Boolean.go
/Users/me/Projects/ps-go/output/src/Control_Category/Control_Category.go
/Users/me/Projects/ps-go/output/src/Control_Semigroupoid/Control_Semigroupoid.go
/Users/me/Projects/ps-go/output/src/Control_Applicative/Control_Applicative.go
/Users/me/Projects/ps-go/output/src/Data_BooleanAlgebra/Data_BooleanAlgebra.go
...
can't load package: package Main: cannot find package "Main" in any of:
	/nix/store/rpn9isjzmg4pqjgy2776xv5zzfysx1ck-go-1.11.6/share/go/src/Main (from $GOROOT)
	/Users/me/Projects/ps-go/go/src/Main (from $GOPATH)

It looks like the generated golang code is being dumped into output, but the compiler is trying to build out of ~/Projects/ps-go/go. The installation instructions are a little vague on how to set up the go workspace. It sounds like you’re supposed to set up a separate GOPATH to avoid co-mingling your own foreign implementation go code with code the compiler is generating.

A step by step example showing the process from beginning to end would be helpful.

Thanks!

Following up on my own post here. I was using an out of date nix channel which brought in an older version of the golang compiler. I updated the channel, and everything works great. Sorry for any false alarms.

2 Likes

If one wanted to ”publish" a package for psgo, would a nice option be to use GitHub versioning with spago? If I recall correctly this should still allow other spago packages to use it.

This may be more of a PureScript implementation question in general, but does psgo employ the Go garbage collector (as is done in the case of JavaScript)? Or is this just gotten for free by using garbage-collected types from in Go?

I’m not sure if the two are mutually exclusive, but the generated code is pretty straightforward and doesn’t do any explicit memory management. So it’s like the generated javascript, in that sense.

1 Like

I haven’t looked into it, but I do need to learn more about spago package management.

Good to have a Golang backend! It’s quite a popular language.

I’ve some questions about PureScript Native I hope you can answer.

  1. Can you explain how it is possible that the C++ backend uses reference counting? I read in the readme that std::shared_ptr is used for memory management. My immediate association on memory management for functional languages is garbage collection, because of cycles one can create in functional program’s and reference counting does not play nicely with cycles. Am I missing something in the case of PureScript?

  2. Do you have any benchmarks comparing performance between the C++ and the Golang backend? I’m really curious how they compare when transpiling a high-level functional language like PureScript.

  3. Have you ever thought about a backend using Malfunction? Malfunction is a “low-level untyped program representation, designed as a target for compilers of functional programming languages.” It essentially is a backdoor into the OCaml compiler, so you can pass it low-level untyped Lambda terms (an intermediate language of the compiler), instead of high-level OCaml code.

    Malfunction/OCaml has the benefit that it is designed for functional languages from the start: tail calls, partial application, over application, enumerations with or without payload, optional laziness, … it is all backed in. I took a quick look at the spec, but it should be quite easy to convert PureScript’s CoreFn directly into Malfunction’s s-expressions.

    In contrast to Haskell Core, Malfunction has strict semantics, as PureScript does. Also, OCaml is know to compile fast native code and has a good performing garbage collector!

1 Like
  1. Retain cycles with ref counting in the C++ backend is indeed a problem and limitation. Right now, it emits a warning during compilation (C++ -> binary), at least for the most common cases.

  2. I don’t have any formal benchmarks right now, but anecdotally the Golang backend seems to perform well vs the C++ backend (in other words, I’m seeing faster). This is probably due to std::shared_ptr not being very fast (for various legitimate reasons).

  3. Definitely! I’ve been following Malfunction for some time, and have been tempted to try it out on numerous occasions. Ultimately the things that stopped me were concerns about about long term support of the project (I couldn’t tell how much they were committed to it), and the fact that multicore was still a WIP. Sounds great, otherwise.

  1. Thanks for the explanation!

  2. Sounds good. Golang is a simple language and there are some good people working on optimisations.

  3. Seems like Stephen is catching up with the latest OCaml compiler. Multicore is still a pain in OCaml indeed… Let me know if you ever start working on it, I’m interested to help!

1 Like

Hi Andy,

It’s been a while, but I took some time to look at your Golang backend. I’ve written down some ideas to make the generated code more performant and more readable. Let me know what you think of it. Could be possible I’m missing some details you already put some thought in and that my ideas are not possible. Just let me know!

1 Like

Hi Tim,

Your optimizations are reasonable goals, and generally consistent with what I tried to generate in the first place with the C++ and then later Go backends. But as you guessed, there are limitations that led to what you see now. To broadly explain them:

  1. The functions are values. Some (non-function) values depend on other values, so there are top-level initialization order issues for those. You could decide to optimize away the wrapper functions for functions at their definition, but then at the places where they are used, there isn’t necessarily the type information needed to distinguish functions from any other kind of values.

  2. For constructors, classes, etc., you’re fine at the points of definition, but for usage, the compiler (corefn) often provides names of types, but not full module information for them, which you would need to qualify the structs you’ve defined in particular modules. This might have changed at some point, or could possibly be addressed with changes to mainline purescript corefn.

I still feel there are opportunites for generated code performance and readability improvements, so I don’t want to discourage you or anyone else from experimenting. I’m always happy to review and merge a PR!

1 Like

Hi Andy!

Can you give an example of both problems? In particular, I don’t see why point 2 is resolved by using dictionaries instead of structs. When CoreFn does nog give us enough info to create a performing backend, we should check how to extend it.

1 Like

Hi Andy,

Anything more on this?

Cheers!

2 Likes