qlang

module
v0.0.1-lw Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2022 License: MIT, MIT

README

Q Progrmaming Language

Q is a toy programming language with a mix of R and Python’s syntax. It was written in Go and inspired by https://interpreterbook.com/.

Usage

go run cmd/q/main.go

Assignments

Both = and <- can be used for assignment. Variable names can contain letters, numbers, and underscores, but must start with a letter.

x = 1
y <- x
x + y
#> 2

There is also a let keyword for variable declaration.

let x <- 1

Q makes a difference between assignment and declartion in the context of nested scopes. This is when we work inside functions, if and for statements. See more details in the section on control structures below.

Data structures

Primitives

Primitive data structures include: numbers (integers and floats), strings and booleans

1 + 1 + (10 * 2) / 4
#> 7
"hello" + " " + "world"
#> "hello world"
!false
#> true
Vectors

Both R and Python has 1-dimensional containers for storing a series of values. Q offers the vector data structure as created by []

[1, 2, 3]
#> [1, 2, 3]

The print() helper shows the vector’s type as well as the number fo elements.

print([1, 2, "hello"])
#> Vector with 3 elements
#> [1, 2, "hello"]

As in R, a vector is typed by its inner elements. Vectors containing only numbers are numeric vectors, vectors with only string elements are character vectors, and so on. A vector with mixed types is simply a base Vector type, similar to a Python list. No type conversion is done automatically, if the elements are heterogeneous the base type will be used.

Vectors in Q have 1-based indexing: the first element starts at index 1, not 0. Built-in functions for vectors include len(), append(), head(), tail()

x = as_vector(1:10)

print(x[1:3])
print(append(x, [11, 12, 13], 14, "15"))
print(head(x, 10))
#> NumericVector with 3 elements
#> [1, 2, 3]
#> Vector with 15 elements
#> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, "15"]
#> NumericVector with 10 elements
#> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Inspired by R, operators are vectorized element-wise. Or in numpy’s terms, they are “broadcasted”.

[1, 2, 3] + [4, 5, 6]
#> [5, 7, 9]
[1, 2, 3] * [4, 5, 6]
#> [4, 10, 18]
["hello ", "good "] + ["world", "morning"]
#> ["hello world", "good morning"]

Elements are recycled only if it has lenght 1 or is a scalar.

[1, 2, 3] * 2
#> [2, 4, 6]
[1, 2, 3] + [4]
#> [5, 6, 7]
[1, 2, 3] + [4, 5]
#> ERROR: Incompatible vector lengths, left=3 and right=2

Boolean indexing works as well

s = random(10, 1, 3)
s[s > 1.5]
#> [2.2093205759592394, 2.881018176090025, 2.3291201064369806, 1.8754283743739604, 1.8492749941425313, 2.373646145734219, 1.6018237211705741]
Dictionaries

You can create a hash table structure in Q called a dictionary with a pair of {, similar to Python, except that you don’t have to quote the keys.

property = "functional"
q = {name: "Q", age: 0, property: true}
print(q)

q["age"] = q["age"] + 1

print(keys(q))
print(values(q))
#> {"age": 0, "functional": true, "name": "Q"}
#> CharacterVector with 3 elements
#> ["age", "functional", "name"]
#> Vector with 3 elements
#> [1, true, "Q"]
Control flows

Q supports for ... in loops and if eles conditions. Both the iteration and condition need to be put in parentheses.

for (name in ["Q", "R", "Python"]) {
  if (name != "Python") {
    print(name)
  } else {
    print("I don't like Python")
  }
}
#> "Q"
#> "R"
#> "I don't like Python"

for in and if blocks have their own scopes. Inside their inner scope, we can (recursively) access and rebind a variable name defined in the outer scope using assignment without the let keyword like so.

result = []
for (i in 1:3) {
  result = append(result, i)
}
result
#> [1, 2, 3]

Or you can create vector with specified length with vector() and then start filling in the elements with indexing.

result = vector(3)
for (i in 1:3) {
  result[i] = i
}
result
#> [1, 2, 3]

However, if we declare a variable in the inner scope with the let keyowrd, that variable will shadow the outer scope variable and get lost when the block ends. For example, the following code will not work as expected.

flag = true
if (flag) {
  let flag = false
}
flag
#> true
Functions

Function definition uses the R style, simply create a function object with the fn keyword and then bind it to a name. Default arguments and named arguments are supported.

add = fn(x, y = 1, z = 1) {
  x + y + z * 2
}

add(1, z = 2)
#> 6

Functions in Q are first-class citizens. They can be passed around as arguments and returned from other functions. There is a return keyword but functions can also use implicit returns. Here we define a map function that takes a function and a vector and applies the function to each element of the vector.

map = fn(arr, f) {
    result = vector(len(arr))
    for (i in arr) {
        result[i] = f(arr[i])
    }
    result
}

[1, 2, 3] |> map(fn(x) x * 2)
#> [2, 4, 6]

Of course the preferred the way to to double a vector is to simply use the vectorized operator *.

Another example of implementing a filter() function that take a vector and a predicate function and returns a vector with only the elements that satisfy the predicate.

filter = fn(x, f) {
  result = []
  for (i in 1:len(x)) {
    if (f(x[i])) {
      result = append(result, x[i])
    }
  }
  result
}

[
  {name: "Ross",   job: "Paleontology"},
  {name: "Monoca", job: "Chef"}
] |> filter(fn(x) x["job"] == "Chef")
#> [{"name": "Monoca", "job": "Chef"}]

Next steps

  • ... for variadic arguments and spread operator

  • index tests for vector and dict

  • dataframe interface

  • improve error message with token col and line

  • more standard library functions

Directories

Path Synopsis
cmd
q
pkg
ast
std

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL