Language Reference: Best Practices

Guidelines for writing clear and concise Pointless code

These best-practices are meant to help you make effective use of the language features of Pointless. You can find detailed descriptions of the language features covered here in the language reference.

Note that this guide focuses on recommendations relating to the core language, rather than the standard library.


Use Compound Assignment

Use compound assignment operators when possible.

score += 1
price |= round
score = score + 1
price = round(price)

Put Statements on Separate Lines

Put statements on separate lines.

x = 6
y = 7
print(x * y)
x = 6; y = 7; print(x * y)

Avoid Getter Functions

Use built-in syntax to access list elements, object values, and table rows and columns.

games[0].score
Obj.get(Table.get(games, 0), "score")

Avoid Setter Functions

Use structural updates instead of setter functions to transform data structures.

evilTwin = player
evilTwin.malice = 100
player.enemies += 1
evilTwin = Obj.set(player, "malice", 100)
player = Obj.set(player, "enemies", player.enemies + 1)

Note that the structural update semantics of Pointless mean that these two ways of updating variables are equivalent.


Use Objects for Records

Use objects to represent records (structures with a fixed number of entries that each have a distinct role). Do not use lists as records.

point = { x: 1, y: 2 }
card = { value: 10, suit: "hearts" }
point = [1, 2]
card = [10, "hearts"]

Don't Concatenate to Update

Use variable updates instead of object concatenation to update objects.

player.health += 1
player += { health: player.health + 1 }

Use Identifier Keys in Records

Use valid identifiers as record keys and table columns.

{ userName: "Clementine", userId: 0 }

#{
  userName     , userId
  "Clementine" ,      0 
  "Ducky"      ,      1 
}
{ "user name": "Clementine", "user-id": 0 }

#{
  "user name"  , "user-id"
  "Clementine" ,         0 
  "Ducky"      ,         1 
}

Omit Quotes from Keys

Omit quotes for keys in record objects.

{ city: "Chicago", state: "IL", population: 2721308 }
{ "city": "Chicago", "state": "IL", "population": 2721308 }

Use Key Punning

Use object key punning when setting an object key to a variable of the same name.

point = { x, y }
point = { x: x, y: y }

Use Dot Syntax

Use . syntax to access and update object keys and table columns when possible.

city.name
city["name"]

Don't Use Lists as Tables

Use tables to store lists of record objects with matching keys.

#{
  city          , state , population
  "New York"    , "NY"  ,    8478072
  "Los Angeles" , "CA"  ,    3878704
  "Chicago"     , "IL"  ,    2721308
  "Houston"     , "TX"  ,    2390125
}
[
  { city: "New York", state: "NY", population: 8478072 },
  { city: "Los Angeles", state: "CA", population: 3878704 },
  { city: "Chicago", state: "IL", population: 2721308 },
  { city: "Houston", state: "TX", population: 2390125 },
]

Align Table Columns

Align and pad table columns for readability.

#{
  city          , state , population
  "New York"    , "NY"  ,    8478072
  "Los Angeles" , "CA"  ,    3878704
  "Chicago"     , "IL"  ,    2721308
  "Houston"     , "TX"  ,    2390125
}
#{
  city, state, population
  "New York", "NY", 8478072
  "Los Angeles", "CA", 3878704
  "Chicago", "IL", 2721308
  "Houston", "TX", 2390125
}

Put Table Rows on Separate Lines

Put table rows on separate lines.

#{
  city          , state , population
  "New York"    , "NY"  ,    8478072
  "Los Angeles" , "CA"  ,    3878704
  "Chicago"     , "IL"  ,    2721308
  "Houston"     , "TX"  ,    2390125
}
#{ city, state, population; "New York", "NY", 8478072; "Los Angeles", "CA", 3878704; "Chicago", "IL", 2721308; "Houston", "TX", 2390125 }

Use Row Lookups

Use the row lookup feature of tables to find individual rows based on unique field values.

philly = cities[{ name: "Philadelphia" }]
philly = (cities ? arg.name == "Philadelphia")[0]

Use Column Operations

When possible, structure table operations around columns rather than rows.

products.price $= arg - 1
products $= arg + { price: arg.price - 1 }

Avoid Zero Length Check

Use isEmpty instead of len(...) == 0 to check for empty data structures.

isEmpty(playlist)
len(playlist) == 0

Use the Not-In Operator

Use the not-in !in operator to write .. !in .. instead of not .. in ...

guess !in words
not guess in words

Use the Fallback Operator

Use the fallback operator ?? to avoid .. !in .. or .. == none conditions for getting fallback values.

size = beverage.size ?? "small"
size =
  if "size" !in beverage or beverage.size == none then
    "small"
  else
    beverage.size
  end

Use Push to Append to a List

Use push to append a single item to a list or table instead of concatenation.

numbers |= push(n)
numbers += [n]

Use Negative Indices

Use negative indices to access items from the end of a list or table.

items[-1]
items[len(items) - 1]

Use Star to Repeat Values

Use the * operator to repeat strings and list elements.

"la" * 5
[0] * 10
"lalalalala"
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Use String Interpolation

Use string interpolation instead of concatenation.

"Hello $person.name!"
"Hello " + person.name + "!"

Omit Interpolation Parentheses

Omit unnecessary parentheses around interpolated variables.

"$price dollars for a pair of socks!?"
"$(price) dollars for a pair of socks!?"

Use Raw Strings

Use raw strings to avoid excessive escape sequences.

r"C:\Users\Ducky\Documents"
"C:\\Users\\Ducky\\Documents"

Minimize Raw String Escapes

Omit unnecessary levels of raw string quote escaping.

r#"Hello "world!""#
r##"Hello "world!""##

Use Multi-Line Strings

Use multi-line strings when they make string contents clearer.

"
▓▓  ▓▓▓ ▓    ▓▓
▓ ▓  ▓  ▓   ▓
▓▓   ▓  ▓    ▓
▓    ▓  ▓     ▓
▓    ▓  ▓▓▓ ▓▓
"
"▓▓  ▓▓▓ ▓    ▓▓\n▓ ▓  ▓  ▓   ▓\n▓▓   ▓  ▓    ▓\n▓    ▓  ▓     ▓\n▓    ▓  ▓▓▓ ▓▓"

Leverage String Alignment

Leverage the automatic trimming and alignment behavior of multi-line strings to format code more legibly.

if printPtls then
  print("
  ▓▓  ▓▓▓ ▓    ▓▓
  ▓ ▓  ▓  ▓   ▓
  ▓▓   ▓  ▓    ▓
  ▓    ▓  ▓     ▓
  ▓    ▓  ▓▓▓ ▓▓
  ")
end
if printPtls then
  print("▓▓  ▓▓▓ ▓    ▓▓
▓ ▓  ▓  ▓   ▓
▓▓   ▓  ▓    ▓
▓    ▓  ▓     ▓
▓    ▓  ▓▓▓ ▓▓")
end

These two strings produce the same output. For details see multi-line strings.


Avoid Redundant Booleans

Don't include redundant boolean comparisons in logical expressions.

if item.onSale then
  print(item.price / 2)
end
if item.onSale == true then
  print(item.price / 2)
end

Use Not

Use not instead of == false.

if not Math.isInt(quantity) then
  print("quantity must be a whole number")
end
if Math.isInt(quantity) == false then
  print("quantity must be a whole number")
end

Use Parentheses in Mixed Boolean Expressions

Use parentheses in expressions that mix different boolean operators (and, or, or not).

isGood and (isFast or isCheap)
isGood and isFast or isCheap

Use Set When Checking Membership

Use sets instead of lists to store a large number of values on which you will be calling has.

scrabbleWords = Set.of(import "lines:scrabble-dict.txt")

has(scrabbleWords, "yeet")
scrabbleWords = import "lines:scrabble-dict.txt"

has(scrabbleWords, "yeet")

Use Match

Use match instead of if when matching an expression with three or more possible values.

match spin
  case "gimel" then
    score += pot
    pot = 0
  case "hei" then
    score += pot / 2
    pot /= 2
  case "shin" then
    score -= 1
    pot += 1
end
if spin == "gimel" then
  score += pot
  pot = 0
elif spin == "hei" then
  score += pot / 2
  pot /= 2
elif spin == "shin" then
  score -= 1
  pot += 1
end

Avoid Nested Conditionals

Flatten nested conditionals when possible.

if n > 0 then
  "positive"
elif n < 0 then
  "negative"
else
  "zero"
end
if n > 0 then
  "positive"
else
  if n < 0 then
    "negative"
  else
    "zero"
  end
end

Refactor Conditional Defs

Lift variable assignments outside of simple conditionals.

sweetener =
  if diet == "vegan" then
    "agave"
  else
    "honey"
  end
if diet == "vegan" then
  sweetener = "agave"
else
  sweetener = "honey"
end

Use Pipelines

Refactor nested function calls into pipeline syntax when possible.

games
  | Table.summarize("team", getStats)
  | sortDescBy("winPct")
  | print
print(sortDescBy(Table.summarize(games, "team", getStats), "winPct"))

Use Map and Filter Operators

Use the map $ and filter ? operators instead of map and filter functions.

numbers
  ? Math.isEven
  $ arg / 2
List.map(List.filter(numbers, Math.isEven), fn(n) n * 2 end)

Omit Parentheses in Pipelines

Omit parentheses for single-argument functions in pipelines.

numbers
  $ Math.sqrt
  | print
numbers
  $ Math.sqrt()
  | print()

Avoid Anonymous Functions

Use top-level function definitions instead of anonymous functions.

fn distance(point, center)
  dx = point.x - center.x
  dy = point.y - center.y
  Math.sqrt(dx ** 2 + dx ** 2)
end

fn averageDistance(points, center)
  points
    $ distance(center)
    | List.average
end
fn averageDistance(points, center)
  distance = fn(point)
    dx = point.x - center.x
    dy = point.y - center.y
    Math.sqrt(dx ** 2 + dx ** 2)
  end

  points
    $ distance
    | List.average
end

Use Arg in Pipelines

Use arg in function pipelines instead of anonymous functions.

words $ translations[arg]
words $ fn(word) translations[word] end

Omit Initial Arg

Omit arg that would otherwise be the first argument in a pipeline function call.

numbers $ Math.roundTo(2)
numbers $ Math.roundTo(arg, 2)

Note that arg cannot be omitted as the first argument if subsequent arguments to the function also use arg, since using arg stops the first argument to a pipeline call from being implicitly passed. Refactor calls like these into new functions to avoid this situation.

-- Best option: refactor into a new function

fn joinFirst(strs)
  join(strs, strs[0])
end

strings | joinFirst
-- Incorrect: wrong number of arguments, since using `arg` in the second
-- argument means that the first argument is no longer implicitly passed

strings | join(arg[0])

-- Correct, but confusing

strings | join(arg, arg[0])

Put the Most Important Parameter First

Put the most important parameter first in a function definition. If a function could be described as transforming, accessing, or analyzing one of its arguments, then that argument is probably the most important.

fn swap(list, indexA, indexB)
  -- Transform `list` by swapping the values at `indexA` and `indexB`
end

fn startsWith(string, prefix)
  -- Check if `string` starts with `prefix`
end
fn swap(indexA, indexB, list)
  -- Transform `list` by swapping the values at `indexA` and `indexB`
end

fn startsWith(prefix, string)
  -- Check if `string` starts with `prefix`
end

Write Pure Functions

Write pure (side effect free) functions when possible.

fn showTask(task)
  if task.done then
    "[x] $task.name"
  else
    "[ ] $task.name"
  end
end

tasks
  $ showTask
  $ print
fn printTask(task)
  if task.done then
    print("[x] $task.name")
  else
    print("[ ] $task.name")
  end
end

tasks $ printTask

Avoid Unnecessary Returns

Don't use return for the final expression in a function.

fn sqrt(n)
  n ** 0.5
end

fn min(a, b)
  if a < b then a else b end
end
fn sqrt(n)
  return n ** 0.5
end

fn min(a, b)
  if a < b then
    return a
  else
    return b
  end
end

Avoid Early Returns

Don't use early return statements that don't simplify your code.

fn min(a, b)
  if a < b then a else b end
end
fn min(a, b)
  if a < b then
    return a
  end

  b
end

Avoid Else After Return

Refactor code to avoid using elif or else after a return statement.

fn findOdd(numbers)
  for n in numbers do
    if Math.isOdd(n) then
      return n
    end

    print("Skipped $n")
  end
end
fn findOdd(numbers)
  for n in numbers do
    if Math.isOdd(n) then
      return n
    else
      print("Skipped $n")
    end
  end
end

Use the Right Function

If a function exists that accomplishes a specific task, choose it over more general functions.

chars("Hello world!")
pop(items)
split("Hello world!", "")
dropLast(items, 1)

Omit Module Prefixes for Globals

Omit module prefixes when calling global functions.

print("Hello world!")
Console.print("Hello world!")

Don't Shadow Built-In Definitions

Don't shadow built-in global functions or module names.

message = "Enter a rating 1-5: "
maximum = 5
list = []
prompt = "Enter a rating 1-5: "
max = 5
List = []

Use Camel Case

Use camel case for multi-word variable, function, table column, and record object key names. Don't capitalize single-word names. Don't capitalize the names of constants.

gameState = "paused"

fn point(x, y)
  { x, y }
end

pi = 3.141592654
gamestate = "paused"

fn Point(x, y)
  { x, y }
end

PI = 3.141592654

Use Leading Zero for Decimals

Include a leading 0 in decimal literals for numbers between 0 and 1.

n = 0.1
n = .1

Use Trailing Commas

When splitting a piece of comma-separated code across multiple lines, include a trailing comma on the last line before the closing delimiter.

days = [
  "Lunes",
  "Martes",
  "Miércoles",
  "Jueves",
  "Viernes",
  "Sábado",
  "Domingo",
]
days = [
  "Lunes",
  "Martes",
  "Miércoles",
  "Jueves",
  "Viernes",
  "Sábado",
  "Domingo"
]

No Import Side Effects

Library code should never have side effects when imported.

fn maximum(a, b)
  if a > b then a else b end
end

fn showInfo()
  print("Maximum™ Version 1.0, patent pending")
end

{ maximum, showInfo }
fn maximum(a, b)
  if a > b then a else b end
end

print("Maximum™ Version 1.0, patent pending")

{ maximum }

Export Objects

Export objects instead of individual definitions.

fn maximum(a, b)
  if a > b then a else b end
end

{ maximum }
fn maximum(a, b)
  if a > b then a else b end
end

maximum

Use Imports to Read from a Known File

Use import with the appropriate specifier to load files from a fixed path relative to your source file.

-- Looks for 'alice.txt' in the source directory
story = import "text:alice.txt"
-- Looks for 'alice.txt' in the call directory
story = Fs.read("alice.txt")

Use Anonymous Loops

Omit unnecessary loop variables.

for 10 do
  print("loading")
end
for n in range(10) do
  print("loading")
end

Use Loop Index Variables

Use a second loop variable to track indices when looping over lists or tables.

for player, index in rankings do
  print("$index $player")
end
index = 0

for player in rankings do
  print("$index $player")
  index += 1
end

Use Loop Value Variables

Use a second loop variable to track values when looping over objects.

for item, quantity in inventory do
  print("$item x $quantity")
end
for item in inventory do
  quantity = inventory[item]
  print("$item x $quantity")
end