Skip to content

Language comparison

Ilkka Törmä edited this page Feb 20, 2018 · 2 revisions

This page works as a secondary tutorial of Husk for people familiar with other golfing languages, like the imperative Pyth, the tacit Jelly and the stack-based CJam and 05AB1E. Other golfing languages exist, but the author is not as familiar with them.

Unique features

There are some distinctive features of Husk that are not present in any of the aforementioned languages.

  • Husk is lazily evaluated, and can handle infinite lists just as easily as finite ones.
  • Husk has no imperative control flow constructs like for loops and if statements. There is a ternary if function, and because of lazy evaluation, it always short-circuits. Similarly, logical and and or are short-circuiting. Iteration is achieved with infinite lists, higher order functions, and/or recursion.
  • Husk is deterministic, meaning that the output of a program is completely determined by the inputs. There are no commands for accessing randomness, the file system, local time etc. There aren't even mutable variables or registers. This is because implementing them while preserving laziness is a pain.
  • Husk has a strict type system in which every expression has a well-defined type. In particular, lists are homogeneous: a list may only contain elements of a single type. The expression [[1,2,3],[[1],[2],[3]]] is not well-typed. A tuple is distinct from a two-element list. It always contains two elements, which may have different types.
  • The type system places some restrictions on functions that operate on lists. In particular, consider the fold/reduce function F. In other languages, you can fold any binary function over a two-element list, and the effect is to call it on the two elements. In Husk, the binary function must have the type a -> a -> a for some type a; this means that the result must have the same type as the arguments. This is because on compile time, the type checker does not know whether the list has 1, 2 of 10 elements, and the types much be compatible in all cases. F also works on tuples, and since a tuple always has two elements, in that case the binary function has no such restriction.

Comparison to Pyth

Husk programs are often written with a prefix syntax, so on the surface they resemble Pyth more closely than Jelly or 05AB1E (although internally the parsing process is quite different). The major differences to Husk are the following.

  • Every Pyth operator has a fixed arity, so parsing is unambiguous. There is no concept of syntactic arity in Husk. Instead, each function grabs some number of arguments in a way that makes the program as a whole well-typed. Functions that grab fewer arguments than expected are partially applied. In particular, a function may grab zero arguments, so that it effectively becomes a leaf of the parse tree. Other functions may grab it as an argument instead.
  • Some Pyth operators, like m (map lambda over a list), produce implicit lambdas with named arguments. Others, like M (map operator over a list), grab one preceding operator and use it as an argument. In Husk, only the latter type of function exists (in this case, also named m). There is no special syntax associated to these functions: they grab one or more arguments using a prefix syntax, just like every other function. The expression mL means "map length", m+3 means "map (add three)"; the argument function +3 is addition (+) partially applied to 3.
  • Pyth uses the variables Q, z, w, E and .z for user input, and their presence in the program changes its semantics slightly. Lambda arguments are various characters that depend on the operator that introduced them. In Husk, superscript digits are used for both program and lambda arguments. Like in Pyth, they are usually introduced implicitly to the end of the program, and their presence may change program semantics. Also, the Husk interpreter takes input as command line arguments, while Pyth uses STDIN.
  • Pyth has the D and L constructs for defining helper functions. Husk can define a helper function on a separate line, and refer to it with a subscript digit.

Comparison to Jelly

Jelly is a tacit language whose syntax is inspired by J. A sequence of Jelly symbols is parsed as a chain in a process where 1-3 links at a time are chopped off from the beginning and form an independent syntactic unit. A link is either a single token or a compound formed by a quick acting on one or more other links. The major differences to Husk are the following.

  • Husk tends to favor overloading functions over automatic vectorization, since the latter is hard to implement within the type system. There are commands for arbitrarily deep vectorization, but the interpreter can't always correctly infer their types.
  • Quicks in Jelly correspond roughly to higher order functions and combinators in Husk. For example, Jelly's (monadic) is analogous to Husk's m, and Jelly's $ and ¥ correspond to Husk's o. In Husk, there is no special syntax for these functions, and you can even apply higher order functions to other higher order functions.
  • The behavior of Jelly quicks may depend on the arities of their arguments. In Husk, there is no concept of syntactic arity, but some higher order functions can inspect the type of a function and change their behavior based on it. For example, Vṗ tests whether a list contains a prime number, while V= tests whether it contains two adjacent equal elements.
  • You can view simple Husk programs as Jelly-like chains. Execution proceeds from right to left, contrary to Jelly. When you apply a binary function, the argument is not implicitly recycled, but a new argument is taken instead. To reuse an argument, you can use superscript digits or combinators.
  • In Jelly, lists are often transformed using the idiom <code>ị, where <code> produces a list of indices and indexes back into the input list. In Husk, higher order functions are used instead due to the lack of vectorized indexing and reused arguments.
  • Like in Jelly, multiline Husk programs are parsed as several functions. The first line is the main function, instead of the last line. Lines are accessed by subscript digits; there are no "previous line" or "this line" commands.

Comparison to CJam and 05AB1E

CJam and 05AB1E are stack-based: there is a global stack on which (almost) all commands operate. Their approach to control flow is slightly different. CJam uses blocks, which are user-defined procedures consisting of a list of commands. Some commands operate on blocks, mapping or folding them over lists, iterating them etc. 05AB1E uses loops and other imperative syntactic constructs.

  • You can view simple Husk programs as stack-based, with execution proceeding from right to left. The inputs are first pushed to the stack. Then, each token grabs zero or more arguments from the stack, acts on them, and pushes the result. This is a nondeterministic process: the function + can either pop two values and push their sum, or pop one value n and push a partially applied function that adds n to its argument, or pop nothing and push the binary addition function; the latter two cases correspond rougly to the CJam blocks {1+} and {+}. Superscript digits push new copies of the inputs. The type inference system guarantees that at the end of execution, a single printable (not a function) value remains on the stack.
  • CJam's block manipulation commands correspond roughly to Husk's higher order functions and combinators. As stated above, each function can be a one-element block on its own, and you can use oȯö to define two-, three- or four-elements blocks, and parentheses for longer ones. However, since functions are handled as black boxes, you cannot inspect or modify their internals.
  • Most Husk functions expect either a function or a concrete argument, but are not overloaded to act on both (# and f are exceptions), whereas in CJam, many operators can be applied to both blocks and other data.
  • CJam and 05AB1E take input from STDIN, while Husk uses command line arguments. Like Husk, 05AB1E supports implicit inputs, as well as explicit inputs via E, I and several other commands. In CJam, input must be fetched explicitly with r, l or q.
  • 05AB1E doesn't use CJam-style blocks, relying on syntactic constructs instead. For example, ε<code>} loops over a list, executes <code> on each of its elements and collects the results in a list (the } is optional at end of line). This corresponds roughly to m<function> in Husk, which maps <function> over a list.
  • In general, the type system of 05AB1E is probably the most different from Husk's, since it is extremely lenient. It allows integers to be treated as strings whenever possible, and some commands modify the entire stack in different ways depending on the type of the top item.
Clone this wiki locally