Tupilled

From Esolang
Jump to navigation Jump to search
Tupilled
Paradigm(s) Functional, High-Level
Designed by User:Proxxa, User:Ashli Katt
Appeared in Category:2023
Type system Dynamic
Computational class Unknown
Reference implementation Unimplemented
File extension(s) .tup, .tuple

Tupilled is a high-level, functional programming language based on tuples and lambdas by User:Ashli Katt and User:Proxxa. All values are tuples or lambdas, so there are no true types, and thus there is no compile-time type checking. Tupilled is highly inspired by Haskell.

The name comes from a shortening of the word "Tuple-pilled."

Syntax Outline

Reserved Tokens

Tupilled has a small set of reserved tokens.

  • (), for tuples.
  • [], as a grouping symbol. They also indicate an evaluation when used as a parameter.
  • {}, for lambda definitions or parameters.
  • =, for assigning functions.
  • ,, for separating tuple elements and function parameters.
  • // indicates a comment.

The following tokens are not reserved but instead hold special meaning.

  • main is an entry point. It must be defined in every Tupilled program.
  • panic will abruptly stop the program.
  • unique every instance is replaced with a unique tuple during parsing. This functionality is useful for user-defined "types."

The grouping symbols and commas are completely disallowed in identifiers. = can appear in an identifier so long as the identifier is not only =. Similarly, . may appear, but .. cannot. a// is an identifier, but a // is an identifier followed by a comment.

Tuples

Tuples are a list of comma-delimited elements wrapped in a set of parentheses, (). As such, () is a valid tuple as is ((), (())). Given there are no numbers in Tupilled, a simple approach to an unsigned integer is creating Peano numbers where () is 0, (()) is 1, ((())) is 2, and so on. Tuples may also contain lambdas.

Functions

Functions follow a similar definition scheme to very simple functions in Haskell.

ident paramA, paramB = body

Identifiers may be any string of characters which are not reserved. Parameters are comma-delimited and may match values as explained later. All function bodies consist of a single statement.

Functions are all equivalent to giving a lambda value to a top-level constant.

Pattern Matching

Function parameters may be pattern-matched in order to destructure arguments or set specific requirements. The parameter name (a) will match a tuple with one inner element and provide that element to the identifier a. Similarly, (a, b) will match a tuple with two inner elements and provide those elements to the identifiers a and b, respectively. An identifier on its own, such as a, will simply match any tuple. For example: snd (a, b) = b

f{a} matches any lambda that accepts one tuple parameter and names it f. a has no meaning here and is used simply as a pattern to match any tuple; it is not usable by the function's body. In a similar way, f{} accepts a lambda with no arguments. f{{}} accepts a lambda that takes another lambda (which takes nothing). {} matches a lambda with 0 arguments, and binds it to nothing. For example: map_pair f{z} (a, b) = (f a, f b)

An underscore may be used to ignore a value, so (_, a) will get the pair's second element, and ignore the first. An underscore can take the place of anything that would otherwise be a name. For example: first a, _ = a accepts two parameters, but only binds the first one.

Brackets have special meaning inside of a pattern, they may be used to match the return value of an expression. [expr] will evaluate expr and match the returned value. For example: first ([Pair], (a, _)) = a will destructure a 2-tuple, its first value must match the return value of Pair (A function), and the rest is destructed as usual.

Piecewise Functions

Functions can be given multiple definitions when all those definitions accept the same number of arguments. This is done by appending a comma to a definition and writing new parameters and a new body.

false = ()
true = (false)

isPeano ()  = true,
        (n) = isPeano n,
        _   = false

In this example, we check if a tuple is a peano number. ie: (), (()), ((())), etc.

When isPeano is called, it will attempt to match the supplied arguments to its patterns first-to-last. It will first check if the argument is the empty tuple, which is Peano 0. If that fails (not a 0-tuple), it will attempt to match the argument to the pattern (n), a 1-tuple, and recursively check if the inner element is a peano. If both of those fail, (2+ tuple), it is not a peano number.

Infixing

A function may be infixed if it takes in two arguments and the function identifier is wrapped in parentheses. As such, a definition of a && operator may be made as such:

false = ()
true = (false)

(&&) [false], _ = false,
     _      , a = a

Infix operations have lower precedence than regular prefix functions. You may need to use [] to group expressions when working with infix functions. The parser will, however, greedily match infix operations. Ex: square 2 + 3 is treated as square [2 + 3] NOT [square 2] + 3

Lambdas

Lambdas, much like they are in other languages, are like nameless functions. In reality, though, functions are named lambdas. They are defined by wrapping a function without its identifier in braces, {}. A function can be defined by providing no arguments before the = and returning a lambda.

wrap = { a = (a) }

Input/Output

All input and output are defined within the main function, which must have either 0 or 1 arguments. When the main function returns, its value is read and the following steps are taken:

  • Create a string, ""
  • Match (0, 1, 2, 3, 4, 5, 6, 7, next). Use values 0 through 7 as bits. () represents 0. All other values represent 1. Construct a byte (ASCII character) using these bits and append it to the end of the string.
  • Repeat the previous process using b as the new output tuple. If b's length is not 9, stop early.
  • When finished, interpret the string as UTF-8 text.

Here is an example of output being parsed:

Tuple                                       | String
----------------------------------------------------------
((), (()), (()), (), (), (), (), (()), ())  | 
()                                          | a

The input is given the same format and is done in reverse. So, if the input is Z, then the data input to main is ((), (()), (), (()), (()), (), (()), (), ()).

There is no mid-program printing.

There is another possible method of considering IO that is also valid, but is incompatible with the previously stated method of creating a string:

  • Create a bitstring, ""
  • Match (bit, next). If bit is (), append 0 to the bistring. Otherwise, append 1.
  • Repeat the previous process using b as the new output tuple. If b's length is not 2, stop early.
  • When finished, interpret the string as UTF-8 text, padding the end with 0s as necessary.

Here is an example of output being parsed:

Tuple                     | Bitstring
--------------------------------------
((()), ((), ((()), ())))  | 
((), ((()), ()))          | 1
((()), ())                | 10
()                        | 101
                          | 00000101

Likewise, input is in the same format, but the process is done in reverse to create the tuple form of the input.

The examples below use the bitstring form of IO as they were written before the current method was conceived.

Examples

Hello World

A program that outputs Hello, World!

main = ((),((()),((),((),((()),((),((),((),((),((()),((()),((),((),((()),((),((()),((),
       ((()),((()),((),((()),((()),((),((),((),((()),((()),((),((()),((()),((),((),((),
       ((()),((()),((),((()),((()),((()),((()),((),((),((()),((),((()),((()),((),((),
       ((),((),((()),((),((),((),((),((),((),((()),((),((()),((),((()),((()),((()),((),
       ((()),((()),((),((()),((()),((()),((()),((),((()),((()),((()),((),((),((()),((),
       ((),((()),((()),((),((()),((()),((),((),((),((()),((()),((),((),((()),((),((),
       ((),((),((()),((),((),((),((),((()))))))))))))))))))))))))))))))))))))))))))
       )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

or, more readable:

0 a = ((), a)
1 a = ((()), a)
. = ()

main = [ 0 1 0 0 1 0 0 0 0 1 1 0 0 1 0 1 0 1 1 0 1 1 0 0 0 1 1 0 1 1 0 0 0 1 1 0 1 1 1 1 
         0 0 1 0 1 1 0 0 0 0 1 0 0 0 0 0 0 1 0 1 0 1 1 1 0 1 1 0 1 1 1 1 0 1 1 1 0 0 1 0 
         0 1 1 0 1 1 0 0 0 1 1 0 0 1 0 0 0 0 1 0 0 0 0 1 . ]

Panic

A program that will always panic.

main = panic

Cat

A cat program will output its input.

main a = a

Reverse Cat

A program that will output the reverse of its input.

main a = reverse a
reverse bitstring = #reverse bitstring ()
#reverse (0, (1, (2, (3, (4, (5, (6, (7, next)))))))) last = #reverse next (0, (1, (2, (3, (4, (5, (6, (7, last))))))))
         _,                                           last = last

Boolean Operations

Implementation of simple boolean operations.

false = ()
true = (false)
bool [false] = false,
             = true

(&)  [true],  [true] = true,
                     = false
(|) [false], [false] = false,
                     = true
!            [false] = true,
                     = false
(^) [false],       x = bool x,
          _,       x = !x

Custom Types

Some custom type implementations using unique

Pair type

Pair_typeid = unique
Pair_new a b = (Pair_typeid, (a, b))

// Same as "first (a _) = a" but with a type id tagged on
Pair_first ([Pair_typeid], (a, _)) = a
Pair_second ([Pair_typeid], (_, a)) = a
Pair_into_inner ([Pair_typeid], inner) = inner
Pair_reverse ([Pair_typeid], (a, b)) = Pair_new b a
// f{t} matches a lambda that takes in a tuple. Currently no designated
// way to describe its output type.
Pair_map([Pair_typeid], (a, b)) f{t} = Pair_new f a f b
// Alternatively Pair_new [f a] [f b]

List type

// First, define numbers for indexing
0 = ()
1 = (0)
inc n = (n)
dec (n) = n
// No other operators, because this is all that is necessary.

LinkedList = unique

// () is the "null" end of the linked list. Otherwise, the second should be (item, rest)
// where item is the "item" of the index and rest is another linked list.
empty_list = (LinkedList, ())

// Append by prepending the "null" end.
list_prepend self([LinkedList], _) it = (LinkedList, (it, self))

list_append empty[empty_list] it = list_prepend empty it,
            ([LinkedList], (item, rest)) new = (LinkedList, (item, list_append rest new))

// Could write () instead of [0], but [0] is more readable as an index
list_get ([LinkedList], (item, _)) [0] = item,
         ([LinkedList], (_, rest)) idx = list_get rest [dec idx]

// very basic aliasing
list_index a b = list_get a b

list_construct_end = unique

// list_construct 1 2 3 list_construct_end
// only issue is magically getting the unique value. The solution would be that alternative secret unique type
list_construct  self([LinkedList], _) [list_construct_end] = self,
                self([LinkedList], _) it = { next = list_construct [list_append self it] next },
                first,                _ = list_construct (LinkedList, ()) first

list_map [empty_list] _{a} = empty_list,
         list([LinkedList], (item, rest)) f{a} = (LinkedList, (f item, list_map rest, f))