-
Notifications
You must be signed in to change notification settings - Fork 6
Embedding Pipefish in Go
By importing the appropriate part of the Pipefish distribution (github.com/tim-hardcastle/Pipefish/source/pf
) as a library, you can embed Pipefish in your own applications, rather than accessing it via the TUI or a client.
The various methods etc are documented formally by godev, so here we will give a tutorial on how to use them in practice.
Many of the available features are only necessary for the most sophisticated users, and you may just want to skip down to the example at the foot of the page and copy that.
Having imported the pf
library, we request a new service using a function with the signature func NewService() *Service
.
At this point, optionally:
-
If you want the service to use a database, this is the point at which you should assign it, since it might be used by
init
commands in the code. You do this with a method having signaturefunc (sv *Service) SetDatabase(db *sql.DB)
. -
If you want the service to reference local external services (as though they were on the same hub) you can do this by passing it a map of strings to services using a method with signature
func (sv *Service) SetLocalExternalServices(svs map[string]*Service)
; again, if you want to do this, you should do so at this stage, before anyinit
command can assume that these services exist. Remote external services work as usual.
Having done either of those things if necessary, we can now compile the service. You can do this either using func (sv *Service) InitializeFromCode(code string) error
or func (sv *Service) InitializeFromFilepath(scriptFilepath string) error
, the names of which are more or less self-explanatory.
func (sv *Service) NeedsUpdate() (bool, error)
will return true
if the source code files for a service have been changed since the service was first initialized.
Then either you can perform func (s *Service) CallMain() (values.Value, error)
, the name of which explains itself); or to evaluate an expression or perform any other command, you use the method func (sv *Service) Do(line string) (Value, error)
.
This will return an non-nil error if there are compile-time errors. If intead the line compiles successfully but there is a runtime error, then that will be returned as the Value
together with a nil
error.
As you can see, what it returns is a Pipefish value, of type pf.Value
, which is a struct having two fields: T
of type pf.Type
, and V
of type any
. You have four choices of what to do with it besides leaving it as it is.
-
Use the method
func (sv *Service) ToLiteral(v Value) string
to convert it into a Pipefish literal. -
Use the method
func (sv *Service) ToString(v Value) string
to apply thestring
method of the service to it. -
Use the method
func (sv *Service) ToGo (pfValue Value, goType reflect.Type) (any, error)
to convert it to a Go value of the given type, if possible. -
Deal with it yourself. To this end we have supplied constants
UNDEFINED_TYPE
,BLING
,OK
,TUPLE
,ERROR
,NULL
,INT
,BOOL
,STRING
,RUNE
,TYPE
,FUNC
,PAIR
,LIST
,MAP
,SET
,LABEL
. There are methodsfunc (sv *Service) IsClone(v Value) bool
,func (sv *Service) IsEnum(v Value) bool
, andfunc (sv *Service) IsStruct(v Value) bool
, which do what you think they would do, and a functionfunc (sv *Service) UnderlyingType(v Value) Type
which returns the parent type of a type if it's a clone, and its own type otherwise. Also, thepf
library exposes the internal types used to represent PipefishValues
. See Appendix C of this wiki for further details of the internal representation of Pipefish values.
To deal with error conditions, we have the following methods:
-
func (sv *Service) IsBroken() bool
will returntrue
if no script has been compiled yet, or if the compilation failed. -
func (sv *Service) ErrorsExist() bool
will returntrue
if the last thing the service did produced errors, whether this was a result of trying to compile the script; of trying to compile a line usingDo
; or of a runtime error caused by executing the line. -
func (sv *Service) GetErrors() []*Error
will get a list of errors. -
func (sv *Service) GetErrorReport() (string, error)
gets the errors summarized in a string. -
func ExplainError(es []*Error, i int) (string, error)
will get you the same explanation you'd get fromhub why
. -
func GetTraceReport(e *err.Error) string
extracts the trace from a runtime error. -
func (sv *Service) GetTrackingReport() (string, error)
gets the tracking data, if any. -
func PrettyString(s string, left, right int) string
converts the strings returned by the previous four methods from markup to highlighted text.
An InHandler
is an interface to a type defining what happens when the Pipefish code performs get x from Input("<prompt>"). It has one method,
Get() string`.
An OutHandler
is an interface to a type defining what happens when the Pipefish code performs post x to Output()
. It has two methods: Out(v Value)
and Write(s string)
, the first of which will typically serialize the Value
in some way before writing it to some output, and the second of which will typically write the string to the same output.
You can set them using the methods func (sv *Service) SetInHandler(in InHandler) error
and func (sv *Service) SetOutHandler(out OutHandler) error
.
To help construct them, there are the following methods and functions:
-
func MakeSimpleInHandler(in io.Reader) *SimpleInHandler
. Returns an InHandler that will get its string from theio.Reader
and do nothing else. -
func MakeTerminalInHandler(prompt string) *TerminalInHandler
. Returns anInhandler
that will get its string from the terminal, prompting the end-user with the given prompt. -
func (sv *Service) MakeLiteralWritingOutHandler(out io.Writer) *SimpleOutHandler
. Returns anOutHandler
that will write the literal of the passed value to the givenio.Writer
. -
func (sv *Service) MakeStringWritingOutHandler(out io.Writer) *SimpleOutHandler
. Returns anOutHandler
that will write the string representation of the passed value to the givenio.Writer
. -
func (sv *Service) MakeLiteralTerminalOutHandler() *SimpleOutHandler
. Returns anOutHandler
that will write the literal of the passed value to the terminal. -
func (sv *Service) MakeStringTerminalOutHandler() *SimpleOutHandler
. Returns anOutHandler
that will write the string representation of the passed value to the terminal.
-
func (sv *Service) GetVariable(vname string) (values.Value, error)
gets the value of a global variable. Unlike doing this using theDo
method,GetVariable
gives you access to private variables. -
func (sv *Service) SetVariable(vname string, ty values.ValueType, v any) error
sets the value of a global variable. Unlike doing this using theDo
method,SetVariable
gives you access to private variables. -
func (sv *Service) GetFilepath() (string, error)
gets the path to the root file of the service. -
func (sv *Service) GetSources() (map[string][]string, error)
gets the source code of the service as a map from filenames to lists of strings (i.e. lines of source code). -
func (sv *Service) TypeToTypeName(t Type) (string, error)
: does what it says: converts something of typepf.Type
to a Go string naming the type, e.g. convertingpf.BOOL
to the string"bool"
. -
func (sv *Service) TypeNameToType(s string) (Type, error)
reversed the previous process and gets apf.Type
from a string representing the type name.
As a complete if simple example, the following Go code will tell you that the eighth Fibonacci number is 21.
package main
import (
"reflect"
"github.com/tim-hardcastle/Pipefish/source/pf"
)
const fibCode = `// Pipefish code for computing the nth Fibonnaci number.
def
fib(n int) :
first from a, b = 0, 1 for i = 0; i < n; i + 1 :
b, a + b
`
func main() {
fibService := pf.NewService()
fibService.InitializeFromCode(fibCode)
pfResult, _ := fibService.Do(`fib 8`)
goResult, _ := fibService.ToGo(pfResult, reflect.TypeFor[int]())
println("The eighth Fibonacci number is", goResult.(int))
}
🧿 Pipefish is distributed under the MIT license. Please steal my code and ideas.