Macros In Finkel

This section shows how to write and use macros. Macros in Finkel are similar to macros in Common Lisp and Clojure. Macros in Finkel are implemented as a function taking codes and returning a code.

Understanding Compilation Phases

During compilation, the compiler executable parses the contents of the source code. If the parsed code was a list, and the first element of the list was a symbol of known macro name, the rest of the elements in the list will be passed to the macro. Resulting forms will be replaced with the original list form of the macro. This replacement of the code with macro function is often called macro expansion. The expanded result will again get expanded until it cannot be expanded anymore. During macro expansion, the compiler can use predefined functions in the executable. To add functions to use during macro expansion, one needs to explicitly tell so.

Defining Macro With eval-when And defmacro

One way to tell a new macro to the compiler is to define a macro inside eval-when (compile). The eval-when is a macro that specifies the phase of declaration in its body form. The phase compile will evaluate the contents while compiling the parsed source code.

Open a new file and save following contents to a file named eval-when.fnk:

;;; File: eval-when.fnk

(defmodule Main
  (import-when [:compile]
    (Finkel.Prelude)))

(eval-when [:compile]
  (defmacro say-hello []
    '(putStrLn "Hello macro!"))
  (defmacro say-bye []
    '(putStrLn "Goodbye.")))

(defn (:: main (IO ()))
  (do (putStrLn ";;; eval-when ;;;")
      (say-hello)
      (say-bye)))

In the above example, (import-when [:compile] (Finkel.Prelude)) is added in the defmodule to import functions and data types for writing while compiling the Main module.

The eval-when macro can take multiple forms. Two forms are passed to eval-when in the above example, one to define a macro named say-hello , and another to define a macro named say-bye.

The say-hello macro takes no argument, and the body of the macro simply returns a quoted form with a single quote (i.e. '). Similarly, the say-bye macro takes no argument and returns a form to prints out a message.

The main function contains the say-hello and say-bye macros. Unlike functions, macros taking no arguments need to be surrounded by parentheses.

One can run the compiler with the -ddump-parsed option to observe the parsed Haskell representation:

$ finkel make -fno-code -ddump-parsed eval-when.fnk

==================== Parser ====================
module Main where
main :: IO ()
main
  = do putStrLn ";;; eval-when ;;;"
       putStrLn "Hello macro!"
       putStrLn "Goodbye."


[1 of 1] Compiling Main             ( eval-when.fnk, nothing )

Defining Macro With macrolet

One can add a temporary macro with the macrolet macro. Following macrolet.fnk example do similar work done in the previous example, but using macrolet instead of eval-when and defmacro.

;;;; File: macrolet.fnk

(defmodule Main
  (import-when [:compile]
    (Finkel.Prelude)))

(macrolet ((say-hello []
             '(putStrLn "Hello macro!"))
           (say-bye []
             '(putStrLn "Goodbye.")))
  (defn (:: main (IO ()))
    (do (putStrLn ";;; macrolet ;;;")
        (say-hello)
        (say-bye))))

Note that single macrolet form can define multiple temporary macros.

$ finkel make -fno-code -ddump-parsed macrolet.fnk

==================== Parser ====================
module Main where
main :: IO ()
main
  = do putStrLn ";;; macrolet ;;;"
       putStrLn "Hello macro!"
       putStrLn "Goodbye."


[1 of 1] Compiling Main             ( macrolet.fnk, nothing )

Loading Macros With require

Another way to add macros to the current module is to require a module containing macros. Open a file named RequireMe.fnk and save the following code:

;;;; File: RequireMe.fnk

(defmodule RequireMe
  (export say-hello say-bye)
  (import (Finkel.Prelude)))

(defmacro say-hello []
  '(putStrLn "Hello macro!"))

(defmacro say-bye []
  '(putStrLn "Goodbye."))

Note that the RequireMe module has the import of Finkel.Prelude inside defmodule. This is because the macros defined in RequireMe are not for itself, but other modules.

Next, open and edit another file named require.fnk to require the RequireMe module:

;;;; File: require.fnk

%p(OPTIONS_GHC -ddump-parsed)

(defmodule Main
  (require
   (RequireMe (say-hello say-bye))))

(defn (:: main (IO ()))
  (do (putStrLn ";;; require ;;;")
      (say-hello)
      (say-bye)))

Compilation output:

$ finkel make -no-link -fbyte-code require.fnk
[1 of 1] Compiling RequireMe        ( RequireMe.fnk, interpreted ) [required]

==================== Parser ====================
module Main where
main :: IO ()
main
  = do putStrLn ";;; require ;;;"
       putStrLn "Hello macro!"
       putStrLn "Goodbye."


[2 of 2] Compiling Main             ( require.fnk, interpreted )

Unlike the previous two examples, one needs to generate an object code of the RequireMe module so that the macro functions defined in RequireMe could be used in the file require.fnk.

Note

As of finkel version 0.1, one may need to add -dynamic-too option to the finkel executable when compiling a source code file containing require.

Quasiquote, Unquote, And Unquote-Splice

Macro can unquote and unquote-splice a form inside quasiquote.

Open a new file named unquote.fnk and save the following contents:

;;;; File: unquote.fnk

(defmodule Main
  (import-when [:compile]
    (Finkel.Prelude)))

(eval-when [:compile]
  (defmacro uq1 [arg]
    `(putStrLn (++ "uq1: arg = " (show ,arg))))

  (defmacro uq2 [arg]
    `(putStrLn ,(++ "uq2: arg = " (show arg)))))

(defn (:: main (IO ()))
  (do (uq1 "foo")
      (uq2 "bar")))

The example defines two macros: uq1 and uq2. Both macros use ` (back-tick) instead of ' (single quote) in body expression.

In uq1, the macro argument arg is unquoted with ,, and the unquoted form is passed as the second argument of ++ function. In uq2 the expression (++ "uq2: arg = " (show arg)) is unquoted with ,.

Observing parsed result with -ddump-parsed:

$ finkel make -fno-code -ddump-parsed unquote.fnk

==================== Parser ====================
module Main where
main :: IO ()
main
  = do putStrLn ("uq1: arg = " ++ show "foo")
       putStrLn "uq2: arg = \"bar\""


[1 of 1] Compiling Main             ( unquote.fnk, nothing )

Parsed Haskell representation shows ++ in the expanded form of uq1 macro. Expanded result of uq2 evaluates ++ at the time of macro expansion, so the resulting form of uq2 is a literal String.

Inside the quasi-quoted form, ,@ is used to unquote-splice a list form. The ,@ can unquote-splice a quoted list and a Haskell list.

;;;; File: unquote-splice.fnk

(defmodule Main
  (import-when [:compile]
    (Finkel.Prelude)))

(eval-when [:compile]
  (defmacro uqs [arg]
    `(putStrLn (concat [,@arg]))))

(defn (:: main (IO ()))
  (do (uqs ("foo" "bar" "buzz"))
      (uqs ["foo" "bar" "buzz"])))

Observing parsed Haskell code:

$ finkel make -fno-code -ddump-parsed unquote-splice.fnk

==================== Parser ====================
module Main where
main :: IO ()
main
  = do putStrLn (concat ["foo", "bar", "buzz"])
       putStrLn (concat ["foo", "bar", "buzz"])


[1 of 1] Compiling Main             ( unquote-splice.fnk, nothing )

Getting Macro Arguments As A List

Macro can take its entire argument as a list form. Below example codes show a macro which takes entire arguments passed to it as a list named args:

;;;; File: arglist.fnk

(defmodule Main
  (import-when [:compile]
    (Finkel.Prelude)))

(eval-when [:compile]
  (defmacro puts args
    `(putStrLn (unwords [,@args]))))

(defn (:: main (IO ()))
  (puts "foo" "bar" "buzz"))

Parsed Haskell code:

$ finkel make -fno-code -ddump-parsed arglist.fnk

==================== Parser ====================
module Main where
main :: IO ()
main = putStrLn (unwords ["foo", "bar", "buzz"])


[1 of 1] Compiling Main             ( arglist.fnk, nothing )

Getting Values From Macro Arguments

One can obtain Haskell values from arguments passed to macro:

;;;; File: fib-macro.fnk

(defmodule Main
  (import-when [:compile]
    (Finkel.Prelude)))

(eval-when [:compile]
  (defn (:: fib (-> Int Int))
    [n]
    (if (< n 2)
        n
        (+ (fib (- n 1)) (fib (- n 2)))))

  (defmacro fib-macro [n]
    (case (fromCode n)
      (Just i) (toCode (fib i))
      Nothing (error "fib-macro: not an integer literal"))))

(defn (:: main (IO ()))
  (print (fib-macro 10)))

The above example applies the fromCode function to the macro argument to get an Int value from the code object. To return the code object, the fib-macro applies toCode to the Int value evaluated by the fib function. Note that the fib function needs to be defined inside eval-when so that fib-macro can use the function during macro expansion.

Sample compilation output:

$ finkel make -fno-code -ddump-parsed fib-macro.fnk

==================== Parser ====================
module Main where
main :: IO ()
main = print 55


[1 of 1] Compiling Main             ( fib-macro.fnk, nothing )

Special forms

The Finkel core keywords are implemented as macros made from Finkel kernel. Details of Finkel core keywords are described in the haddock API documentation of the finkel-core package.

This section explains built-in macros in the Finkel kernel language. These built-in macros are sometimes called special forms. All special forms start with :, followed by lower case alphabetic character, to avoid name conflict with existing Haskell functions.

:begin

The :begin special form is basically for writing a macro returning multiple top-level declarations. Following code shows an example use of :begin, to return type synonym declarations from the nat-types macro:

;;;; File: begin.fnk

%p(LANGUAGE DataKinds)

(defmodule Main
  (import-when [:compile]
    (Finkel.Prelude))
  (import (Data.Proxy)))

(data Nat
  Zero
  (Succ Nat))

(macrolet ((nat-types [n]
             (let ((:: go (-> Int Code Int [Code]))
                   (= go stop body i
                     (if (< stop i)
                         []
                         (let ((= name (make-symbol (++ "N" (show i))))
                               (= next `('Succ ,body)))
                           (: `(type ,name ,body)
                              (go stop next (+ i 1)))))))
               (case (fromCode n)
                 (Just m) `(:begin
                             ,@(go m ''Zero 0))
                 Nothing (error "not an integer")))))
  (nat-types 6))

(defn (:: main (IO ()))
  (print (:: Proxy (Proxy N6))))

Observing parsed Haskell code:

$ finkel make -fno-code -ddump-parsed begin.fnk

==================== Parser ====================
module Main where
import Data.Proxy
data Nat = Zero | Succ Nat
type N0 = 'Zero
type N1 = 'Succ 'Zero
type N2 = 'Succ ('Succ 'Zero)
type N3 = 'Succ ('Succ ('Succ 'Zero))
type N4 = 'Succ ('Succ ('Succ ('Succ 'Zero)))
type N5 = 'Succ ('Succ ('Succ ('Succ ('Succ 'Zero))))
type N6 = 'Succ ('Succ ('Succ ('Succ ('Succ ('Succ 'Zero)))))
main :: IO ()
main = print (Proxy :: Proxy N6)


[1 of 1] Compiling Main             ( begin.fnk, nothing )

:eval-when-compile

The :eval-when-compile special form is used to implement eval-when macro in the core language. Basically, (:eval-when-compile BODY1 BODY2 ...) is the same as (eval-when (compile) BODY1 BODY2 ...).

The following code shows sample use of :eval-when-compile. The function wrap-actions is defined inside :eval-when-compile, so that later the compiler can use the function in the doactions macro.

;;;; File: eval-when-compile.fnk

(defmodule Main
  (import-when [:compile]
    (Finkel.Prelude)))

(:eval-when-compile
  (defn (:: wrap-actions (-> [Code] Code))
    [actions]
    `(do ,@actions)))

(macrolet ((doactions [xs]
             (case (unCode xs)
               (HsList actions) (wrap-actions actions)
               _ (error "doactions: expecting HsList"))))
  (defn (:: foo (-> Int (IO ())))
    [n]
    (doactions [(putStrLn "from foo")
                (print (+ n 1))]))
  (defn (:: bar (-> Int Int (IO ())))
    [a b]
    (doactions [(putStrLn "from bar")
                (print (+ a (* b 2)))])))

(defn (:: main (IO ()))
  (do (foo 41)
      (bar 10 16)))

Parsed Haskell code:

$ finkel make -fno-code -ddump-parsed eval-when-compile.fnk

==================== Parser ====================
module Main where
foo :: Int -> IO ()
foo n
  = do putStrLn "from foo"
       print (n + 1)
bar :: Int -> Int -> IO ()
bar a b
  = do putStrLn "from bar"
       print (a + (b * 2))
main :: IO ()
main
  = do foo 41
       bar 10 16


[1 of 1] Compiling Main             ( eval-when-compile.fnk, nothing )

:quote

The :quote special form is used for quoting the given value as a code object. The ' is syntax sugar of this special form. Internally, quoted values are passed to functions exported from the finkel-kernel package.

Following code shows how underlying Finkel kernel functions are applied to literal values in source code:

;;;; File: quote.fnk

(defmodule Main
  (import (Finkel.Prelude)))

(defn (:: main (IO ()))
  (do (putStrLn ";;; quote ;;;")
      (print 'foo)
      (print (:quote foo))
      (print '42)
      (print (:quote 42))
      (print '"string")
      (print (:quote "string"))))

Parsed Haskell source:

$ finkel make -fno-code -ddump-parsed quote.fnk

==================== Parser ====================
module Main where
import Finkel.Prelude
main :: IO ()
main
  = do putStrLn ";;; quote ;;;"
       print (qSymbol "foo" "quote.fnk" 8 15 8 18)
       print (qSymbol "foo" "quote.fnk" 9 22 9 25)
       print (qInteger 42 "quote.fnk" 10 15 10 17)
       print (qInteger 42 "quote.fnk" 11 22 11 24)
       print (qString "string" "quote.fnk" 12 15 12 23)
       print (qString "string" "quote.fnk" 13 22 13 30)


[1 of 1] Compiling Main             ( quote.fnk, nothing )

:quasiquote

The :quasiquote is the underlying special form for the ` syntax sugar. Inside a quasi-quoted form, :unquote and :unquote-splice could be used for getting the value from the code. Indeed, , is a syntax sugar of :unquote, and ,@ is a syntax sugar of :unquote-splice.

;;;; File: quasiquote.fnk

(defmodule Main
  (import (Finkel.Prelude)))

(defn (:: with-sugar [Code])
  [`(foo ,(length "123") bar)
   `(foo ,@[True False] bar)])

(defn (:: without-sugar [Code])
  [(:quasiquote (foo (:unquote (length "123")) bar))
   (:quasiquote (foo (:unquote-splice [True False]) bar))])

(defn (:: main (IO ()))
  (print (== with-sugar without-sugar)))

Above example prints True:

$ finkel make -o quasiquote quasiquote.fnk
[1 of 1] Compiling Main             ( quasiquote.fnk, quasiquote.o )
Linking quasiquote ...
$ ./quasiquote
True

:require

The :require is for adding a module to the compiler during macro expansion. It also adds macros defined in the required module to the current compiler environment. This special form is used by the defmodule macro.

;;;; File: raw-require.fnk

(:require Finkel.Prelude)

(defmodule Main)

(eval-when [:compile]
  (defmacro say-hello []
    '(putStrLn "Hello macro!"))
  (defmacro say-bye []
    '(putStrLn "Goodbye.")))

(defn (:: main (IO ()))
  (do (putStrLn ";;; raw-require.fnk ;;;")
      (say-hello)
      (say-bye)))

Parsed Haskell code:

$ finkel make -fno-code -ddump-parsed raw-require.fnk

==================== Parser ====================
module Main where
main :: IO ()
main
  = do putStrLn ";;; raw-require.fnk ;;;"
       putStrLn "Hello macro!"
       putStrLn "Goodbye."


[1 of 1] Compiling Main             ( raw-require.fnk, nothing )

:with-macro

The :with-macro is the underlying special form for macrolet macro. This special form is perhaps not useful unless one wants to write an alternative implementation of the macrolet macro. See the source code of Finkel.Core module for usage.