Intermediate

Functions in SNOBOL

Learn how to define, call, and return from functions in SNOBOL, including recursion, local scope, built-in functions, and success/failure returns

Functions in SNOBOL look unlike functions in almost any modern language. There is no def, function, or return-type keyword. Instead, a function is created at runtime by calling the built-in DEFINE, which registers a name, its parameters, and its entry point. The body is just ordinary SNOBOL statements sitting in your program, reached by a goto when the function is called.

What ties it all together is SNOBOL’s defining trait: every statement either succeeds or fails, and functions participate in that flow directly. A function returns its value by assigning to a variable that shares its own name and then transferring to the special label RETURN. If something goes wrong, it can transfer to FRETURN instead — and the call itself fails, letting the caller branch on :S(...)F(...) exactly as it would for a pattern match.

In this tutorial you’ll define functions with parameters, return values, and local variables; write a recursive factorial; explore SNOBOL’s many built-in functions; and use failure returns to signal “no result.” Because SNOBOL is dynamically and weakly typed, a single function can return a string, an integer, or a pattern depending on what it computes.

Defining and Calling a Function

A function is declared with DEFINE('NAME(PARAM1,PARAM2)'). After the DEFINE statement you immediately goto a label past the function body, so the body isn’t executed while the program is “falling through.” The body lives at a label matching the function name, and it returns by assigning to the variable named after the function and jumping to RETURN.

Create a file named functions.sno:

1
2
3
4
5
6
7
8
9
*       Define GREET, taking one parameter NAME
        DEFINE('GREET(NAME)')        :(GREET_END)
*       Function body - entry point is the label GREET
GREET   GREET = "Hello, " NAME "!"   :(RETURN)
GREET_END
*       Now call it
        OUTPUT = GREET("World")
        OUTPUT = GREET("SNOBOL")
END

The :(GREET_END) on the DEFINE line skips over the body during normal flow. When GREET("World") is called, control jumps to the GREET label, builds the greeting by concatenation, stores it in the variable GREET, and :(RETURN) hands that value back to the caller.

Recursion

SNOBOL functions can call themselves, and recursion is the natural way to express repeated computation that returns a value. Here is the classic factorial. The predicate EQ(N,0) succeeds (returning the null string) only when N equals 0, which gives us the base case.

Create a file named recursion.sno:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
*       Recursive factorial
        DEFINE('FACT(N)')            :(FACT_END)
FACT    FACT = EQ(N,0) 1             :S(RETURN)
        FACT = N * FACT(N - 1)       :(RETURN)
FACT_END
*       Call the function for a few values
        OUTPUT = "0! = " FACT(0)
        OUTPUT = "5! = " FACT(5)
        OUTPUT = "10! = " FACT(10)
END

On the line FACT = EQ(N,0) 1, if N is 0 the EQ predicate succeeds, its null result concatenates with 1 to give "1", and :S(RETURN) returns immediately. If EQ fails, the whole statement fails, the assignment never happens, and control falls through to the recursive multiplication.

Local Variables and Scope

By default, names in SNOBOL are global. To get a local variable, list it after the parameter list in DEFINE: 'DOUBLE(X)TEMP' makes X a parameter and TEMP a local. SNOBOL saves the previous value of each local on entry and restores it when the function returns, so the function can’t clobber a global of the same name.

Create a file named scope.sno:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
*       DOUBLE takes parameter X and uses local TEMP
        DEFINE('DOUBLE(X)TEMP')      :(DOUBLE_END)
DOUBLE  TEMP = X + X
        DOUBLE = TEMP                :(RETURN)
DOUBLE_END
*       A global named TEMP that must survive the call
        TEMP = "global value"
        OUTPUT = DOUBLE(21)
        OUTPUT = TEMP
END

Even though DOUBLE assigns to TEMP internally, the global TEMP is untouched because the local declaration shadows it for the duration of the call.

Built-in Functions

SNOBOL4 ships with a rich set of built-in functions for string work. You call them exactly like your own functions. A few of the most useful:

  • SIZE(s) — number of characters in s
  • REVERSE(s)s reversed
  • DUPL(s, n)s repeated n times
  • REPLACE(s, set1, set2) — translate each character of s found in set1 to the matching character in set2
  • TRIM(s) — remove trailing blanks

Create a file named builtins.sno:

1
2
3
4
5
6
7
*       Exploring SNOBOL's built-in functions
        OUTPUT = "SIZE('SNOBOL'): " SIZE('SNOBOL')
        OUTPUT = "REVERSE('SNOBOL'): " REVERSE('SNOBOL')
        OUTPUT = "DUPL('ab', 3): " DUPL('ab', 3)
        OUTPUT = "REPLACE('CAT', 'ABC', 'XYZ'): " REPLACE('CAT', 'ABC', 'XYZ')
        OUTPUT = "TRIM('hi   '): [" TRIM('hi   ') "]"
END

REPLACE('CAT', 'ABC', 'XYZ') maps AX, BY, CZ, leaving any character not in 'ABC' unchanged — so C becomes Z, A becomes X, and T stays T.

Returning Success or Failure

This is where SNOBOL functions shine. A function returns through RETURN on success or FRETURN on failure. When a function takes the FRETURN path, the call expression itself fails, so the caller can branch with :S(...)F(...). Here FIRSTVOWEL returns the first vowel of a word, or fails if there is none.

Create a file named failure.sno:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
*       Return the first vowel, or fail if there is none
        DEFINE('FIRSTVOWEL(WORD)')              :(FV_END)
FIRSTVOWEL
        WORD ANY('aeiouAEIOU') . FIRSTVOWEL     :S(RETURN)F(FRETURN)
FV_END
*       The call fails when no vowel exists
        V = FIRSTVOWEL("sky")                   :S(HAVE1)F(NONE1)
HAVE1   OUTPUT = "sky -> " V                    :(TEST2)
NONE1   OUTPUT = "sky has no vowel"
TEST2   V = FIRSTVOWEL("hello")                 :S(HAVE2)F(NONE2)
HAVE2   OUTPUT = "hello -> " V                  :(DONE)
NONE2   OUTPUT = "hello has no vowel"
DONE
END

The pattern WORD ANY('aeiouAEIOU') . FIRSTVOWEL matches the first vowel and, via the conditional assignment operator ., stores it in FIRSTVOWEL. If no character matches, the match fails and :F(FRETURN) makes the whole call fail — so FIRSTVOWEL("sky") causes the assignment to fail and the caller jumps to NONE1.

Running with Docker

1
2
3
4
5
6
7
8
9
# Pull the official image
docker pull esolang/snobol:latest

# Run each example
docker run --rm -v $(pwd):/app -w /app esolang/snobol:latest snobol functions.sno
docker run --rm -v $(pwd):/app -w /app esolang/snobol:latest snobol recursion.sno
docker run --rm -v $(pwd):/app -w /app esolang/snobol:latest snobol scope.sno
docker run --rm -v $(pwd):/app -w /app esolang/snobol:latest snobol builtins.sno
docker run --rm -v $(pwd):/app -w /app esolang/snobol:latest snobol failure.sno

Expected Output

functions.sno:

Hello, World!
Hello, SNOBOL!

recursion.sno:

0! = 1
5! = 120
10! = 3628800

scope.sno:

42
global value

builtins.sno:

SIZE('SNOBOL'): 6
REVERSE('SNOBOL'): LOBONS
DUPL('ab', 3): ababab
REPLACE('CAT', 'ABC', 'XYZ'): ZXT
TRIM('hi   '): [hi]

failure.sno:

sky has no vowel
hello -> e

Key Concepts

  • Functions are defined at runtimeDEFINE('NAME(PARAMS)LOCALS') registers a function; there is no compile-time def keyword.
  • The body is reached by goto — put :(label_END) on the DEFINE line to skip the body during normal flow; the entry point is the label matching the function name.
  • Return value is the function-named variable — assign to NAME inside the body and transfer to RETURN; the value handed back is whatever NAME holds.
  • Functions can succeed or failRETURN succeeds, FRETURN fails, and the call fails too, so callers branch with :S(...)F(...) just like a pattern match.
  • Locals are declared after the parameter list — names listed after the parens (e.g. 'DOUBLE(X)TEMP') are saved and restored across the call; everything else is global.
  • Recursion is natural — a function may call itself; predicates like EQ, LT, and GT give you base cases by succeeding or failing.
  • Dynamic, weak typing — a single function can return a string, an integer, or a pattern, and SNOBOL converts between numbers and their string forms automatically.
  • No default parameters — SNOBOL has no default-argument syntax; omitted arguments simply arrive as the null string, which you can test for explicitly. Indirect calls are possible via APPLY(name, args...) and the $ indirection operator.

Running Today

All examples can be run using Docker:

docker pull esolang/snobol:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining