G Goat Language

Синтаксис Goat

Полное описание синтаксических конструкций языка с примерами.

Комментарии

Goat поддерживает два вида комментариев:

-- Однострочный комментарий

{- Многострочный
   комментарий -}

fun add x y = x + y  -- inline комментарий

Литералы

Базовые типы и их литеральный синтаксис:

ТипПримерыОписание
Int0, 42, -764-битное целое
Float3.14, -0.5, 1.0e1064-битное число с плавающей точкой
Booltrue, falseЛогический тип
String"hello", "line\n"Строка в UTF-8
Unit()Единичный тип, аналог void

Escape-последовательности в строках

ПоследовательностьЗначение
\nПеренос строки
\tТабуляция
\rВозврат каретки
\\Обратный слеш
\"Двойная кавычка
\'Одиночная кавычка

Операторы

Операторы перечислены от наименьшего к наибольшему приоритету:

ПриоритетОператорАссоциативностьОписание
1|>левоPipe: x |> ff x
1~>левоЛенивый pipe: аргумент оборачивается в lazy
2&&левоЛогическое И
2||левоЛогическое ИЛИ
3== != /=нетРавенство / неравенство (!= и /= — синонимы)
3< > <= >=нетСравнение
4++правоКонкатенация строк и списков
5::правоCons: добавить элемент в начало списка
6+ -левоСложение / вычитание
7* / modлевоУмножение / деление / остаток
8**правоВозведение в степень
9notпрефиксЛогическое отрицание
10-префиксУнарный минус
11применениелевоf x y — применение функции
-- Примеры операторов
let a = 2 ** 10              -- 1024
let b = 10 mod 3            -- 1
let s = "hello" ++ " world" -- "hello world"
let xs = 1 :: 2 :: 3 :: []  -- [1, 2, 3]
let r = xs |> reverse |> show  -- pipeline

let — привязка значения

Привязывает имя к значению. Бывает глобальной (объявление верхнего уровня) и локальной (внутри выражения через in).

Глобальная привязка

let pi = 3.14159
let greeting = "Hello, world!"
let doubled = map (\x -> x * 2) [1..5]

Локальная привязка (let … in)

let result =
  let x = 10 in
  let y = x * 2 in
  x + y       -- 30
Примечание: В IO-блоках let без in является последовательной привязкой, а let! — монадической привязкой IO-действия.

fun — определение функции

Объявляет именованную функцию. Параметры перечисляются через пробел после имени. Тело следует после =.

fun greet name =
  "Hello, " ++ name ++ "!"

fun add x y = x + y

fun clamp lo hi x =
  if x < lo then lo
  else if x > hi then hi
  else x

Каррирование и частичное применение

Все функции каррированы. fun add x y = x + y эквивалентно fun add x = \y -> x + y. Частичное применение создаёт новую функцию:

let add5 = add 5       -- функция Int -> Int
let r    = add5 3     -- 8
let doubled = map (add 0) [1..5]

Лямбда-выражения

Анонимные функции задаются через \params -> body:

let square = \x -> x * x
let add     = \x y -> x + y
let evens   = filter (\x -> x mod 2 == 0) [1..10]
let pairs   = map (\x -> (x, x * x)) [1..5]

Рекурсия

Рекурсивные функции объявляются с ключевым словом rec:

fun rec factorial n =
  if n <= 1 then 1
  else n * factorial (n - 1)

fun rec fibFast a b n =
  if n == 0 then a
  else fibFast b (a + b) (n - 1)
Без rec имя функции недоступно внутри её тела. Взаимная рекурсия пока не поддерживается.

if / then / else

Условное выражение. Всегда должна быть ветка else — обе ветки должны возвращать значение одного типа.

let sign x =
  if x > 0 then "positive"
  else if x < 0 then "negative"
  else "zero"

let abs x = if x < 0 then -x else x

match — сопоставление с образцом

Сопоставляет значение с последовательностью образцов (паттернов). Каждая ветка: | паттерн => тело.

fun describe n =
  match n with
  | 0   => "zero"
  | 1   => "one"
  | _   => "many"

fun sumList xs =
  match xs with
  | []         => 0
  | x :: rest  => x + sumList rest

Гарды (when)

После паттерна можно добавить условие через when:

fun classify n =
  match n with
  | x when x < 0   => "negative"
  | x when x == 0  => "zero"
  | x when x < 10  => "small"
  | _              => "large"

fun safeDiv x y =
  match y with
  | 0            => None
  | d when d > 0 => Some (x / d)
  | d            => Some (-(x / (-d)))

Списки и диапазоны

Литеральные списки

let empty  = []
let nums   = [1, 2, 3, 4, 5]
let mixed  = ["a", "b", "c"]

Cons-оператор ::

let xs = 1 :: 2 :: 3 :: []  -- [1, 2, 3]
let ys = 0 :: xs             -- [0, 1, 2, 3]

Диапазоны

let r1 = [1..5]       -- [1, 2, 3, 4, 5]
let r2 = [0..9]       -- [0, 1, ..., 9]
let r3 = [1..100]     -- все от 1 до 100

Конкатенация списков

let combined = [1, 2] ++ [3, 4]  -- [1, 2, 3, 4]

Tuple

Кортежи фиксированного размера, могут содержать значения разных типов:

let pair   = (1, "hello")
let triple = (1, true, 3.14)

fun fst pair =
  match pair with
  | (a, _) => a

fun swap pair =
  match pair with
  | (a, b) => (b, a)

Алгебраические типы данных

Объявляются через type TypeName = | Ctor1 | Ctor2 of field1 field2 | …. Конструкторы с полями применяются как функции.

type Shape =
  | Circle    of radius
  | Rectangle of width height
  | Triangle  of a b c

type Color =
  | Red
  | Green
  | Blue
  | Custom of r g b

let c  = Circle 5.0
let r  = Rectangle 4.0 6.0
let t  = Triangle 3.0 4.0 5.0

fun area shape =
  match shape with
  | Circle r        => pi * r * r
  | Rectangle w h   => w * h
  | Triangle a b c  =>
      let s = (a + b + c) / 2.0 in
      sqrt (s * (s - a) * (s - b) * (s - c))

Ленивые вычисления

Goat использует явную ленивость: вычисление откладывается только там, где это указано явно.

lazy / force

let expensive = lazy (factorial 100)  -- thunk, не вычисляется
let result     = force expensive       -- вычисляется при force

let thunk = lazy (2 + 2)
let val   = force thunk               -- 4, мемоизируется

Бесконечные списки

Ленивые хвосты позволяют строить бесконечные структуры данных:

fun rec natsFrom n = n :: lazy (natsFrom (n + 1))
let nats = natsFrom 0

fun rec fibs a b = a :: lazy (fibs b (a + b))
let fibStream = fibs 0 1

fun rec repeat x = x :: lazy (repeat x)

-- Взять первые N элементов из бесконечного потока
let first10 = take 10 nats       -- [0..9]
let fibs10  = take 10 fibStream   -- [0, 1, 1, 2, 3, 5, ...]

Ленивый pipe ~>

Передаёт правый операнд как lazy-значение:

-- x ~> f  эквивалентно  f (lazy x)
let stream = fibs 0 1
let filtered = lazyFilter (\x -> x mod 2 == 0) stream

Pipe-операторы

Передают значение слева в функцию справа:

-- |> — обычный pipe
let result = [1..10]
  |> filter (\x -> x mod 2 == 0)
  |> map (\x -> x * x)
  |> fold (\acc x -> acc + x) 0
  |> show
  -- "220" (4² + 6² + 8² + 10² = 220)

IO-блоки

IO-вычисления изолированы в блоках io { … }. Внутри блока доступны специальные операторы:

КонструкцияСмысл
do! exprВыполнить IO-действие, игнорировать результат
let! x = exprВыполнить IO-действие, связать результат с x
let x = exprЧистая привязка (без IO)
return exprВернуть чистое значение из IO-блока
fun main _ =
  io {
    do! println "Как вас зовут?"
    let! name = input
    let msg = "Привет, " ++ name ++ "!"
    do! println msg
    do! writeFile "log.txt" msg
  }

Точка входа

Программа должна определять функцию main, принимающую один аргумент (как правило _) и возвращающую IO-блок:

fun main _ =
  io {
    do! println "Hello, Goat!"
  }

Образцы (patterns) — полный список

ОбразецСовпадает сПример
_Любым значением (wildcard)| _ => "other"
ЛитералКонкретным значением| 0 => "zero"
ПеременнаяЛюбым; связывает значение| x => x + 1
[]Пустым списком| [] => 0
p :: restНепустым списком; голова и хвост| x :: xs => x
[p1, p2]Списком точно с N элементами| [a, b] => a + b
(p1, p2)Tuple соответствующей длины| (a, b) => a
Ctor p1 p2Конструктором ADT| Circle r => pi * r * r
p as nameОбразцом; также связывает всё значение| x :: _ as xs => (x, xs)
p when guardОбразцом и условием| x when x > 0 => "pos"
fun firstTwo xs =
  match xs with
  | []            => (0, 0)
  | [x]           => (x, 0)
  | [x, y]        => (x, y)
  | x :: y :: _  => (x, y)

fun describeList xs =
  match xs with
  | [] => "empty"
  | [_] => "singleton"
  | _ :: _ as all when length all > 10 => "long"
  | _ => "short"