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.

  1. Use Compound Assignment
  2. Separate Statements
  3. Avoid Getter Functions
  4. Avoid Setter Functions
  5. Use Objects for Records
  6. Don't Concatenate to Update
  7. Omit String Key Quotes
  8. Use Identifier Keys
  9. Use Key Punning
  10. Use Dot Syntax
  11. Don't Use Lists as Tables
  12. Use Row Lookups
  13. Use Column Operations
  14. Avoid Zero Length Check
  15. Use Push for Single Items
  16. Use Negative Indices
  17. Use Star to Repeat Values
  18. Use String Interpolation
  19. Omit Interpolation Parens
  20. Use Raw Strings
  21. Minimize Raw String Escapes
  22. Use Multi-Line Strings
  23. Leverage String Alignment
  24. Avoid Redundant Booleans
  25. Use Not
  26. Use Boolean Parentheses
  27. Use Set Membership
  28. Use Match
  29. Avoid Nested Conditionals
  30. Refactor Conditional Defs
  31. Use Pipelines
  32. Use Map and Filter Operators
  33. Omit Pipe Parentheses
  34. Avoid Anonymous Functions
  35. Use Arg in Pipelines
  36. Omit Initial Arg
  37. Put Primary Parameter First
  38. Write Pure Functions
  39. Avoid Unnecessary Returns
  40. Avoid Early Returns
  41. Avoid Else After Return
  42. Use Specific Functions
  43. Omit Module Prefixes
  44. Don't Shadow Globals
  45. Use Camel Case
  46. Use Leading Zero for Decimals
  47. Use Trailing Commas
  48. No Import Side Effects
  49. Export Objects
  50. Use Fixed-Path Imports
  51. Use Anonymous Loops
  52. Use Loop Index Variables
  53. Use Loop Value Variables

Use Compound Assignment

Use compound assignment operators when possible.

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

Separate Statements

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)

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 record objects.

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

Omit String Key Quotes

Omit quotes for keys in record objects.

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

Use Identifier Keys

Use valid identifiers as record object keys and table columns.

{ userName: "Clementine", userId: 0 }

Table.of([
  { userName: "Clementine", userId: 0 },
  { userName: "Ducky", userId: 1 },
])
{ "user name": "Clementine", "user-id": 0 }

Table.of([
  { "user name": "Clementine", "user-id": 0 },
  { "user name": "Ducky", "user-id": 1 },
])

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.

Table.of([
  { 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 },
])
[
  { 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 },
]

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 Push for Single Items

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 Parens

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 Boolean Parentheses

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 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 Pipe Parentheses

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 Primary 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 Specific Functions

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

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

Omit Module Prefixes

Omit module prefixes when calling global functions.

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

Don't Shadow Globals

Don't shadow global built-in functions.

message = "Enter a rating 1-5: "
maximum = 5
prompt = "Enter a rating 1-5: "
max = 5

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

(Yes, the standard library module variables are capitalized; do as I say, not as I do)


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 delimeter.

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 Fixed-Path Imports

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

story = import "text:alice.txt"
-- Won't work if script is called outside the source 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