G Goat Language

Примеры программ

Полные программы из каталога examples/ с подробными объяснениями. Каждый пример демонстрирует отдельную группу возможностей языка.

Факториал — рекурсия и IO

Классический пример рекурсивной функции. Демонстрирует fun rec, условие if/then/else и IO-блок.

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

fun main _ =
  io {
    do! println (show (factorial 0))
    do! println (show (factorial 1))
    do! println (show (factorial 5))
    do! println (show (factorial 10))
  }

Что здесь происходит

  • fun rec — объявляет рекурсивную функцию. Без rec имя функции недоступно внутри тела.
  • Базовый случай — n <= 1 возвращает 1.
  • Рекурсивный случай — n * factorial (n - 1). Скобки важны: без них n - 1 был бы отдельным применением.
  • show конвертирует Int в String для вывода.
  • do! выполняет IO-действие в монадическом блоке.

Вывод

1
1
120
3628800

Числа Фибоначчи — три подхода

Демонстрирует наивную рекурсию, хвостово-рекурсивную оптимизацию и бесконечный ленивый поток.

fibonacci.goat
-- Наивная рекурсия: O(2^n)
fun rec fib n =
  match n with
  | 0 => 0
  | 1 => 1
  | n => fib (n - 1) + fib (n - 2)

-- Хвостовая рекурсия: O(n)
fun rec fibFast a b n =
  if n == 0 then a
  else fibFast b (a + b) (n - 1)

fun main _ =
  io {
    do! println "Naive fibonacci:"
    do! println (show (map fib [0..9]))
    do! println "Fast fibonacci:"
    do! println (show (map (fibFast 0 1) [0..9]))
  }

Бесконечный поток (из lazy_streams)

fun rec fibs a b = a :: lazy (fibs b (a + b))

let fibStream = fibs 0 1

take 10 fibStream  -- [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

a :: lazy (fibs b (a + b)) — голова списка вычисляется сразу, хвост откладывается до вызова force (что делает take внутри).

Операции со списками

Демонстрирует базовые операции: сортировку, свёртку, отображение, фильтрацию и zip.

list_ops.goat
fun rec quicksort xs =
  match xs with
  | []       => []
  | p :: rest =>
      let smaller = filter (\x -> x < p) rest in
      let greater = filter (\x -> x >= p) rest in
      append (quicksort smaller) (p :: quicksort greater)

fun sumList xs = fold (\acc x -> acc + x) 0 xs

fun main _ =
  io {
    let nums = [5, 3, 8, 1, 9, 2, 7, 4, 6]
    do! println (show (quicksort nums))
    do! println (show (sumList [1..10]))
    do! println (show (map (\x -> x * x) [1..5]))
    do! println (show (filter (\x -> x mod 2 == 0) [1..10]))
    do! println (show (zip [1..5] ["a", "b", "c", "d", "e"]))
  }

Вывод

[1, 2, 3, 4, 5, 6, 7, 8, 9]
55
[1, 4, 9, 16, 25]
[2, 4, 6, 8, 10]
[(1, "a"), (2, "b"), (3, "c"), (4, "d"), (5, "e")]

Сортировки — функции сравнения

Параметризованный quicksort: функция сравнения передаётся как аргумент, что позволяет сортировать в любом порядке и по любому критерию. Также mergesort для сравнения.

quicksort.goat
fun rec quicksort cmp xs =
  match xs with
  | []       => []
  | p :: rest =>
      let smaller = filter (\x -> cmp x p) rest in
      let greater = filter (\x -> not (cmp x p) && x /= p) rest in
      let equal   = filter (\x -> x == p) rest in
      append (quicksort cmp smaller)
             (append (p :: equal) (quicksort cmp greater))

fun rec merge xs ys =
  match (xs, ys) with
  | ([], ys)            => ys
  | (xs, [])            => xs
  | (x :: xt, y :: yt) =>
      if x <= y then x :: merge xt ys
      else y :: merge xs yt

fun main _ =
  io {
    let nums = [64, 34, 25, 12, 22, 11, 90]
    do! println (show (quicksort (\a b -> a < b) nums))
    do! println (show (quicksort (\a b -> a > b) nums))
    let strs = ["banana", "apple", "cherry"]
    do! println (show (quicksort (\a b -> a < b) strs))
  }

Ключевые идеи

  • Функция cmp — произвольный компаратор, передаётся как аргумент (функция высшего порядка).
  • Одна реализация покрывает сортировку по возрастанию, убыванию и произвольному критерию.
  • Tuple-паттерн (xs, ys) в merge позволяет сопоставлять несколько значений одновременно.

Ленивые потоки — бесконечные последовательности

Демонстрирует явную ленивость: бесконечные потоки, решето Эратосфена и степени двойки.

lazy_streams.goat
-- Решето Эратосфена
fun rec sieve xs =
  match xs with
  | []       => []
  | p :: rest =>
      p :: lazy (sieve (lazyFilter (\x -> x mod p /= 0) (force rest)))

let primes = sieve (natsFrom 2)

-- Числа Фибоначчи как поток
fun rec fibs a b = a :: lazy (fibs b (a + b))
let fibStream = fibs 0 1

-- Степени числа
fun rec powersOf base n = n :: lazy (powersOf base (n * base))
let twoPowers = powersOf 2 1

fun main _ =
  io {
    do! println (show (take 10 primes))
    do! println (show (take 10 fibStream))
    do! println (show (take 10 twoPowers))
    do! println (show (take 5 (drop 100 nats)))
  }

Разбор решета Эратосфена

  • natsFrom 2 — бесконечный поток 2, 3, 4, 5, …
  • sieve берёт голову (простое число), форсирует хвост, фильтрует кратные и рекурсивно просеивает — всё лениво.
  • force rest явно форсирует ленивый хвост перед передачей в lazyFilter.
  • lazyFilter снова создаёт ленивый хвост, поэтому решето работает по требованию.

Вывод

[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
[100, 101, 102, 103, 104]

Option-монада — безопасные вычисления

Показывает, как использовать Option для безопасных вычислений без исключений. Функции-цепочки через bindOption.

option_monad.goat
fun safeDivide x y =
  if y == 0 then None
  else Some (x / y)

fun safeHead xs =
  match xs with
  | []      => None
  | x :: _ => Some x

fun safeSqrt x =
  if x < 0 then None
  else Some (sqrt (toFloat x))

-- Цепочка safe-операций через bindOption
fun pipeline x y =
  bindOption (safeDivide 100 x) (\q ->
  bindOption (safeSqrt q) (\r ->
  bindOption (safeDivide (toInt r) y) (\result ->
  Some result)))

fun main _ =
  io {
    do! println (show (pipeline 4 2))   -- Some 2
    do! println (show (pipeline 0 2))   -- None
  }

Как работает цепочка

bindOption opt f работает так: если opt равен None — немедленно возвращает None, не вызывая f. Если Some x — передаёт x в f. Это позволяет строить цепочки безопасных вычислений: первый же None «замыкает» цепочку.

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

Показывает объявление ADT, конструкторы с несколькими полями и исчерпывающий pattern matching для вычисления площади и периметра.

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

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))

fun perimeter shape =
  match shape with
  | Circle r        => 2.0 * pi * r
  | Rectangle w h   => 2.0 * (w + h)
  | Triangle a b c  => a + b + c

fun main _ =
  io {
    let shapes = [Circle 5.0, Rectangle 4.0 6.0, Triangle 3.0 4.0 5.0]
    do! println (unlines (map describeShape shapes))
    let largest = fold (\acc s -> if area s > area acc then s else acc)
                       (head shapes) (tail shapes)
    do! println ("Largest: " ++ showShape largest)
  }

Вывод

Circle(r=5.0) area=79 perim=31
Rect(4.0x6.0) area=24 perim=20
Triangle(3.0,4.0,5.0) area=6 perim=12

Largest: Circle(r=5.0)

Работа с файлами

Демонстрирует запись, чтение и обработку файлового содержимого.

file_io.goat
fun main _ =
  io {
    let path = "examples/tmp_output.txt"
    do! writeFile path "Hello from Goat!\nLine 2\nLine 3\n"
    let! content = readFile path
    do! println "File contents:"
    do! println content
    let lineCount = length (lines content)
    do! println ("Lines: " ++ show lineCount)
    let wordList = words content
    do! println ("Words: " ++ show (length wordList))
  }

Разница do! и let!

КонструкцияИспользование
do! action Выполнить IO-действие ради побочного эффекта (вывод, запись файла). Результат игнорируется.
let! x = action Выполнить IO-действие и связать результат с именем x (чтение файла, ввод).
let x = expr Чистая привязка внутри IO-блока — без выполнения IO.