Skip to content

Instantly share code, notes, and snippets.

@i-am-the-slime
Last active February 5, 2021 08:36
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save i-am-the-slime/1ad096a51d155e619dae64f9829c2804 to your computer and use it in GitHub Desktop.
Save i-am-the-slime/1ad096a51d155e619dae64f9829c2804 to your computer and use it in GitHub Desktop.

Getting started with Purescript Native

Prerequisites

In order to be ready to start working with purescript native, you will need three things:

  1. Go
  2. The psgo compiler
  3. The spago package manager

The document below describes where to get each of those.

Install the Go platform for your system

Downloads - The Go Programming Language

Bildschirmfoto 2020-01-06 um 19.57.23.png

Add this to ~/.config/fish/config.fish:

# Go
set -x GOPATH $HOME/go
set -g fish_user_paths "/usr/local/go/bin" $fish_user_paths

Install the Purescript compiler

Download

Releases · purescript/purescript · GitHub

Put it on your PATH

cp ~/Downloads/purescript/purs /usr/local/bin

Install the PSGO compiler

Download

Releases · andyarvanitis/purescript-native · GitHub

If you are on Linux you will have to compile the binaries yourself as described in the README.

Bildschirmfoto 2020-01-07 um 21.02.38.png

Put it on your PATH

Example for macOS:

cp ~/Downloads/psgo /usr/local/bin

Install the package manager spago

Download the binaries from here.

And put it on your path as above.

cp ~/Downloads/spago /usr/local/bin

Try if it works

Run this in a Terminal:

mkdir hello-psgo
cd hello-psgo
spago init

Make psgo the backend

Add the line , backend = "psgo" to the file spago.dhall to look like this:

{-
Welcome to a Spago project!
You can edit this file as you like.
-}
{ name = "my-project"
, dependencies = [ "console", "effect", "psci-support" ]
, packages = ./packages.dhall
, sources = [ "src/**/*.purs", "test/**/*.purs" ]
, backend = "psgo"
}

Run the sample project

spago run

Congratulations 🥳

Setting up VSCode for use with psgo

The next thing we tackle is setting up an editor.

In the bar on the left of the screen, select the icon with four squares (extensions). Then type in purescript in the search box. bd492ed7.png

You should find PureScript IDE. Click on Install.

Next find the Settings by looking for Preferences -> Settings

Change to JSON and add the following entries:

    "purescript.addSpagoSources": true,
    "purescript.buildCommand": "spago build",
    "purescript.codegenTargets": [
        "corefn"
    ],
    "purescript.packagePath": ".spago"

Add the following keyboard shortcuts:

{
    "key": "cmd+i",
    "command": "purescript.addExplicitImport"
},
{
    "key": "cmd+e",
    "command": "editor.action.marker.next"
}

That way you can import the name under your cursor by pressing ⌘+i in the editor.

Also when you hit ⌘+e it will take you to the next error in the document. This in conjunction with saving can come in very handy.

Calling go code from psgo

This post describes how to call go code from your PureScript code in psgo. This is also often referred to as the Foreign Function Interface (FFI) because we are interfacing with functions that are foreign to the PureScript language.

Prerequisites

This guide assumes you have gone through the getting started tutorial and have successfully created a Hello World psgo program with spago.

One time setup:

The steps outlined in this section have to be performed only once per project. Once you are done with this, whenever you would like to add more FFI files after, you can refer to the next section.

This process involves the following steps:

  1. Setting up a folder that will contain all your FFI files
  2. Creating a go.mod file in that folder
  3. Linking that go module up in your main go.mod

Create a folder for your local FFI files:

This folder will contain all the local FFI files you will write for the current project.

mkdir ffi

Create a file called go.mod in the ffi folder with the following content:

module project.localhost/ffi

Modify the file go.mod in the root folder thusly:

After the existing lines starting with replace add the following:

replace project.localhost/ffi => ./ffi

Add the following line to the require block towards the end:

	project.localhost/ffi v0.1.0 // indirect

This diff shows the additions:

additions to the file

Add a new FFI module

Now you're ready to add the first FFI module to your project.

Here's the gist of what needs to be done:

  1. Add an entry in ffi_loader.go
  2. Add a folder in ffi
  3. Add the go file in a subfolder of ffi
  4. Add the PureScript file in src

For this example we will make go print the words "Hello from Go".

Create an entry in purescript-native/ffi_loader.go

Change the bottom of the file to say:

// Add your own FFI packages here.

import (
	_ "project.localhost/ffi/my-first-ffi"
)

Create a folder in ffi

We will create folders in the ffi folder that we created in the previous section.

A folder here has the granularity of a PureScript library. It may contain multiple files.

In accordance with the name chosen above name the folder: my-first-ffi.

Write the Go file

We will create but one file that we call Go_Hello.go. The convention here is to call the file according to the module name under which it appears in the PureScript code. In this case this would be Go.Hello.

This file lives in ffi/my-first-ffi/Go_Hello.go

As an example, here is the full content of the file:

package my_first_ffi

import (
	"fmt"
	. "github.com/purescript-native/go-runtime"
)

func init() {
	exports := Foreign("Go.Hello")

	exports["hello"] = func() Any {
		fmt.Println("Hello from Go!")
		return nil
	}
}

Create the PureScript file

Create the following file as src/Go/Hello.purs:

module Go.Hello where

import Prelude
import Effect (Effect)

foreign import hello :: Effect Unit

Now in order to try it out change your Main.purs file to

module Main where

import Prelude

import Effect (Effect)
import Go.Hello (hello)

main :: Effect Unit
main = hello

Do:

spago run

And you should see:

Hello from Go!

As your output.

Congratulations! You have successfully created a new FFI module for psgo and called go code from PureScript!

Explanations:

Let's go through the lines of Go_Hello.go one by one:

We start with:

package my_first_ffi

Conventionally this is the same as the folder name except that dashes become underscores.

Then there's an import statement which imports:

	. "github.com/purescript-native/go-runtime"

This gives the go runtime which is a tiny (43 lines) library. That mostly provides type aliases for writing Go/PureScript FFI.

Next is this bit:

func init() {
  exports := Foreign("Go.Hello")

Here we define the entry function for this module (always called init). We also initialise our exports which is the dictionary of functions that this module will expose. We specify the exact module name that we will call later from PureScript: "Go.Hello".

What then follows is our export, the function hello:

exports["hello"] = func() Any {
  fmt.Println("Hello from Go!")
  return nil
}

It is a function that takes zero arguments func() and it returns Any.

Let's compare this to the PureScript:

foreign import hello :: Effect Unit

Effect really stands for a function that takes no arguments and Unit is a type with only one possible value unit (hence the name).

So the func() fits Effect but Any is a type alias for interface{} which really could be anything.

We always type non-function types in Go FFI as Any. It is left to the programmer to ensure that they actually use the correct types.

In order to return a unit we simply do return nil in the go code.

PSGO FFI Examples

Import a String

Purescript:

foreign import aString :: String

Go:

exports["aString"] = "hi!"

Function from String to Int

Purescript:

foreign import strLen :: String -> Int

Go:

exports["strLen"] = func(str_ Any) Any {
  str := str_.(string)
  return len(str)
}

In this example len needs a string. Therefore we need to use an FFI pattern where we follow the argument name with an underscore (str_) and then cast to a string further down. The other direction of widening the type of the result of len(str) is no problem and there is no cast needed to go from int to Any.

Functions with multiple parameters

Purescript:

foreign import addInts :: Int -> Int -> Int

Go:

exports["addInts"] = func(n1_ Any) Any {
  return func(n2_ Any) Any {
    n1 := n1_.(int)
    n2 := n2_.(int)
    return n1 + n2
  }

Be careful to always specify a return type with Any.

Because functions in PureScript are always curried the FFI for functions with multiple arguments becomes quite unwieldy.

To make this more bearable you can install the functions library with spago install functions. Then the following works:

Purescript:

foreign import add3IntsImpl :: Fn3 Int Int Int Int

add3Ints :: Int -> Int -> Int -> Int
add3Ints = runFn3 addIntsUncurried

Go:

exports["add3IntsImpl"] = func(n1_ Any, n2_ Any, n3_ Any) Any {
    n1 := n1_.(int)
    n2 := n2_.(int)
    n3 := n3_.(int)
    return n1 + n2 + n3
}

Importing types

Let's take the example of sql

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment