9
\$\begingroup\$

tinylisp is, in its essence, a very stripped-down version of Lisp, as the name suggests. It was made by @DLosc for an "interpret this language" challenge, which can be found here. It features a small amount of builtins, which can be used to create practically anything.

There is a repository for the "official" tinylisp interpreter/documentation here, and it is available to try online here.

What tips do you have for golfing (creating the shortest program possible) in tinylisp? This question is looking for tips to golf specifically in tinylisp, not general tips that can be applied to any language.

\$\endgroup\$

5 Answers 5

5
+500
\$\begingroup\$

Abuse builtins for type-checking

The official way to check the type of a value is with the type builtin, which returns one of Int, List, Name, or Builtin. However, (e(type x)(q Int)) is pretty verbose, and (type? x Int) with the library isn't too much shorter.

The good news here is that errors in tinylisp generally aren't fatal--they just print a warning to stderr and return () (nil). So under certain circumstances, we can use the one-letter builtins in unintended ways to perform ad-hoc type-checking. Supposing we have some value bound to the name x:

  • (a x 1) - truthy for all integers except -1; falsey for -1; error for any other type
  • (s x 1) - same, but replace -1 with 1
  • (a x 0) - same, but with 0
  • (t x) - truthy for lists with more than 1 element; falsey for nil and single-element lists; error for any other type
  • (h x) - truthy for nonempty lists with a truthy first element; falsey for nil and lists with a falsey first element; error for any other type
  • (c()x) - truthy for lists; error for any other type
  • (l()x) - truthy for nonempty lists; falsey for nil; error for any other type
  • (l 0 x) - truthy for positive integers; falsey for 0 and negative integers; error for any other type
  • (v x) - truthy for nonzero integers; falsey for 0 and nil; error for any list that's not a valid s-expression (including any nonempty list of integers); result varies for valid s-exprs and names

Even the string builtins, though longer, could be useful under certain circumstances:

  • (string x) - truthy for integers, builtins, names, and lists of integers (including nil); error for lists containing anything other than integers
  • (chars x) - truthy for nonempty names; falsey for empty name; error for any other type

For example, here's a function that adds all the integers in a ragged list such as (1 2 (3 4 (5) 6)):

(d S(q((L)(i L(i(e(type L)(q Int))L(a(S(h L))(S(t L))))0

(We could write (type? L Int) for the type-check if we loaded the library, but that costs more bytes unless we need the library for something else.)

By the time execution reaches (e(type L)(q Int)), we know that L is either a nonempty list or a nonzero integer. We want a test that's truthy if L is an integer and falsey/error if L is a list. (a L 0) fits the bill:

(d S(q((L)(i L(i(a L 0)L(a(S(h L))(S(t L))))0

But in this case, so does (v L). That's because:

  • v on any integer returns the integer unchanged, and we know the integer won't be zero, so it's always truthy
  • v on any list tries to evaluate the list as tinylisp code
  • Nil evaluates to itself, but we know the list is nonempty
  • A nonempty list evaluates as a function call, where the first element of the list is the function or macro. An integer isn't a valid function or macro; neither is nil; and a nonempty list will get evaluated as a function call, simply trying the same thing one layer down. Upshot: v will always error on a nonempty ragged list of integers.

Total savings: 13 bytes.

(d S(q((L)(i L(i(v L)L(a(S(h L))(S(t L))))0
\$\endgroup\$
2
\$\begingroup\$

Use lambdas instead of full functions

To save a few bytes, you can make your answer a lambda instead of a full function:

Predecessor function:
(q (
  (n)
  (s n 1)))

Golfed:
(q((n)(s n 1)))

This is only useful if the function doesn't recurse.

\$\endgroup\$
2
\$\begingroup\$

Take advantage of parenthesis autocompletion

The tinylisp parser will fill in missing close parens at the end of each line, provided that each of your top-level expressions is on a single line (which it should be anyway for code golf). So instead of this:

(load library)(d D(q((M)(i(h M)(c(h(h M))(D(map t(t M))))()))))

you can write this, for a savings of 5 bytes:

(load library
(d D(q((M)(i(h M)(c(h(h M))(D(map t(t M))))(

(Just make sure that your test code in the header or footer is formatted the same way, or you may get some puzzling error messages.)

\$\endgroup\$
2
\$\begingroup\$

Use the library

Although tinylisp itself has a small set of builtins, it also comes with a standard library that adds a lot of functionality. You can access any function in the library by running (load library).

The names of library functions/constants are not golfy like the builtins are, so some of them aren't worth it. For example, there's no need to load the library just to use inc or dec, since (a 1 x) is the same length as (inc x) and (a(some-expr)1) is shorter than (inc(some-expr)). For others, it depends on the use case: using reverse is probably shorter than reimplementing it yourself, but you may be able to restructure your code to avoid using it at all. Others, like *, are indispensable (multiplication isn't built in, and reimplementing it would be prohibitively long).

One drawback: there's not really any documentation besides just reading the code.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ worth mentioning in the readme, maybe? I think i'll submit a PR with the docs for these \$\endgroup\$
    – Razetime
    Jan 25, 2022 at 4:55
2
\$\begingroup\$

Try switching the order of expressions

When you have multiple atoms (names/integers) next to each other, they have to be separated by whitespace. In some cases, you can rearrange the expression to eliminate the whitespace. A common example is with addition:

(a 1(subexpression))
(a(subexpression)1)

A less obvious case is when you have a function with multiple arguments. Consider a function F with arguments (A B). If you're calling the function like this:

(F A(s B 1))

then you may be able to save a byte by taking the arguments as (B A) instead:

(F(s B 1)A)

(These particular examples only work if they're not at the end of a line; in that case, parenthesis autocompletion would make both options the same length. But there are instances where switching the order helps even at the end of a line.)

Reordering can be especially helpful with conditionals. Frequently, you will have a recursive function with a body structured like this:

(i(condition)(recursive call)(base case))

In some instances, you can save bytes by inverting the condition and reversing the order of the truthy and falsey branches:

(i(inv-condition)(base case)(recursive call))

For example, here's a function F that returns the length of a number's Collatz orbit:

(load library
(d F(q((N)(i(l 1 N)(a(F(i(odd? N)(a(* 3 N)1)(/ N 2)))1)0

If we change the condition (l 1 N) (truthy if N is greater than 1, falsey if N is 1 or less) to (l N 2) (truthy if N is less than 2, falsey if N is 2 or greater), we can switch the truthy and falsey branches and save a byte thanks to parenthesis autocompletion:

(d F(q((N)(i(l N 2)0(a(F(i(odd? N)(a(* 3 N)1)(/ N 2)))1

In this particular case, we can then save more bytes by rewriting (a(...)1 to (a 1(...:

(d F(q((N)(i(l N 2)0(a 1(F(i(odd? N)(a(* 3 N)1)(/ N 2
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.