Kapusta

Kapusta is a Lisp for the Ruby runtime.

It is inspired by Fennel. Kapusta aims to bring the simplicity and joy of Lisp to Ruby. Where Fennel uses Lua's stdlib and runtime, Kapusta uses Ruby's.

For more information about Kapusta, see the official Fennel documentation and tutorials, but replace Lua with Ruby.

Features

  1. Compiles to readable Ruby.
  2. Compiled .rb files don't depend on Kapusta. Run with plain ruby, or load .kap files at runtime via require 'kapusta'.
  3. Two-way Ruby interop.

Install

gem install kapusta

It installs three executables:

  1. kapusta
  2. kapfmt
  3. kapusta-ls

Use

kapusta examples/fizzbuzz.kap

or

exe/kapusta examples/fizzbuzz.kap

or

kapusta --compile examples/fizzbuzz.kap > examples/fizzbuzz.rb
ruby examples/fizzbuzz.rb

For mruby 3 compatible output, such as DragonRuby, use:

kapusta --compile --target=mruby3 examples/match.kap > examples/match-mruby3.rb

Use from Ruby

Ruby can require a .kap file and use it directly.

require 'kapusta'
Kapusta.require('./bank-account', relative_to: __FILE__)
 = BankAccount.new('Ada', 100)

See examples/bank-account.kap and examples/use_bank_account.rb.

Examples

See examples/ and examples-compiled/.

(fn ack [m n]
  (if (= m 0) (+ n 1)
      (= n 0) (ack (- m 1) 1)
      (ack (- m 1) (ack m (- n 1)))))

(print (ack 2 3))
(print (ack 3 3))

Compiles to:

def ack(m, n)
  if m == 0
    n + 1
  elsif n == 0
    ack(m - 1, 1)
  else
    ack(m - 1, ack(m, n - 1))
  end
end

p ack(2, 3)
p ack(3, 3)

Calls and lookup

Hash lookup uses :.

user:name
(: user :name)
(?: user :profile :name) ; safe lookup

Method calls use ..

user.name
(. user :name)

Kapusta source always uses require.

(require :app.args)
(local messages (require :app.messages))
(require "./config")

Compiled Ruby uses the Ruby form that fits:

require "app/args"
require "app/messages"
messages = App::Messages
require_relative "config"

For larger programs, organize code with Kapusta's Ruby host forms:

  1. Use module + defn for stateless functions.
  2. Use class + fn for state or dependencies.
  3. Avoid returning hashes of functions as the main app structure.

Comparison with Fennel

Kapusta keeps most core Fennel forms. The main differences come from Ruby's runtime and object model.

Fennel Kapusta
Lua stdlib Ruby stdlib
:foo is a Lua string :foo is a Ruby symbol
(. xs 1) is the first element (: xs 0) is the first element
string.format, table.insert, etc. use Ruby methods and stdlib instead
(print x) is Lua's print (bare) (print x) is Ruby's p (inspect-style)
(.. "x: " nil) errors at runtime (.. "x: " nil) produces "x: " (Ruby nil.to_s)
with-open, tail! not provided

Kapusta-specific additions:

  • module and class for Ruby host structure, including file-header forms
  • (end) closes a bodyless file-header
  • (defn name ...) or (fn class.name ..)
  • ivar or @var / cvar or @@var / gvar or $var
  • try / catch / finally plus raise for exceptions
  • (ruby "...") raw host escape hatch
  • pass Ruby keyword arguments by ending a call with a symbol-keyed hash: (File.open path "r" {:encoding "UTF-8"})
  • pass a Ruby block by ending a call with a (fn ...) or #(...) literal: (File.open path "r" (fn [io] io.read))

Format

kapfmt --fix examples/fizzbuzz.kap

LSP

Use kapusta-ls in the editor of your choice.

Syntax highlight

For Vim, you can use vim-syntax/.

License

MIT