Про функціональності Go

Наскільки об'єктно Go орієнтований багаторазово і емоційно обговорювалося. Тепер спробуємо оцінити наскільки він функціональний. Зауважимо відразу, оптимізацію хвостової рекурсії компілятор не робить. Чому б? «Це не потрібно в мові з циклами. Коли програміст пише рекурсивний код, він хоче представляти стек викликів або він пише цикл.» — зауважує в листуванні Russ Cox. У мові зате є повноцінні lambda, closure, рекурсивні типи і ряд особливостей. Спробуємо їх застосувати функціональним методом. Приклади здадуться синтетичними тому, що по перше написані негайно виконуються в пісочниці і написані на процедурному все ж мовою у других. Передбачається знайомство з Go так і з функціональним програмуванням, роз'яснень але мало код прокоментував.

Closure, замикання реалізовано мовою в класичній і повною мірою.
Наприклад ліниву рекурсивні послідовність можна отримати так
func produce(source int перестановки func(int) int) func() int {
return func() int { //першокласна lambda
source = перестановок(source) //замикання source
return source
}
}

Простий варіатор для псевдовипадкових чисел
func mutate(int j) int {
return (1664525*j + 1013904223) % 2147483647
}

І ось наш генератор випадкових чисел
next := produce(1, mutate)
next()

Працюючий приклад
package main

import (
"fmt"
)

func produce(source int перестановки func(int) int) func() int {
return func() int { //першокласна lambda
source = перестановок(source) //замикання source
return source
}
}

//простий варіатор для псевдовипадкових чисел
func mutate(int j) int {
return (1664525*j + 1013904223) % 2147483647
}
func main() {

next := produce(1, mutate) //і ось наш генератор випадкових чисел

fmt.Println(next())
fmt.Println(next())
fmt.Println(next())
fmt.Println(next())
fmt.Println(next())

}

Спробувати в пісочниці
Currying. каррирование, застосування функції до одного з аргументів в загальному випадку не реалізовано. Приватні завдання однак вирішуються. Наприклад функція відкладеного виклику стандартної бібліотеки time має сигнатуру func AfterFunc(d Duration, f func()) *Таймер приймає аргументом func(), а ми б хотіли передати щось більш параметрами func(arg MyType). І ми можемо це зробити так
type MyType string //оголошення типу
func (arg MyType) JustPrint(){ //оголошення методу
fmt.Println(arg)
}

метод Go це функція приймає першим аргументом свого бенефіціара
Вираз MyType.JustPrint дасть нам цю функцію з сигнатурою func(arg MyType), яку ми можемо застосувати до аргументу MyType.JustPrint(«З'їж мене»)
Навпаки вираз arg.JustPrint дасть нам функцію JustPrint застосовану до arg c сигнатурою func() яку ми можемо передати нашому будильнику
timer := time.AfterFunc(50 * time.Millisecond, arg.JustPrint)

Працюючий приклад
package main

import (
"fmt"
"time"
)

type MyType string //оголошення типу
func (arg MyType) JustPrint() { //оголошення методу
fmt.Println(arg)
}

func main() {
arg := MyType("Привіт") //екземпляр типу
time.AfterFunc(50*time.Millisecond, arg.JustPrint)
arg = "By"
time.AfterFunc(75*time.Millisecond, arg.JustPrint)
time.Sleep(100 * time.Millisecond)

}

Спробувати в пісочниці.
Continuation, продовження як першокласний об'єкт не реалізований з тієї елегантністю що в scneme. Є між тим вбудована функція panic() приблизний аналог long_jump здатна перервати обчислення і повернути при цьому досягнутий результат(наприклад помилку) в місце, звідки був зроблений виклик. Конструкцію panic(), defer recover() крім обробки винятків можна застосувати наприклад для наскрізного виходу з зайшла занадто глибоко рекурсії(що зауважимо й робиться в пакеті encoding.json). У цьому сенсі конструкція первоклассна, а не виняткова. Вихід з непотрібної рекурсії, варто підкреслити, це класичне застосування continuation.
Ось прямолінійна, не оптимізована(не застосовувати у production!!) рекурсивна функція віддає n-е число Фібоначчі як суму попередніх
func Fib(n int) int {
if (n == 0 {
return 0
}
if (n == 1 {
return 1
}
first := Fib(n - 1)
second := Fib(n - 2)
if first > max { //Якщо числа стали надмірно великі
panic(second) //то тут обчислення перериваються зі збором врожаю
}
return first + second
}

Так ми її викличемо з продовженням(call/cc) бажаючи отримати n-е число Фібоначчі, якщо тільки воно не більше max
var int max = 200
func CallFib(int n) (res int) {
defer func() {
if r := recover(); r != nil { //відновлення продовження
res = r.(int) //плоди праць
}
}()
res = Fib(n)
return
}

Працюючий приклад.
package main

import "fmt"

var int max = 1000

func Fib(n int) int {
if (n == 0 {
return 0
}
if (n == 1 {
return 1
}
first := Fib(n - 1)
second := Fib(n - 2)
if first > max { //Якщо числа стали надмірно великі
panic(second) //то тут обчислення перериваються зі збором врожаю
}
return first + second
}
func CallFib(int n) (res int) {
defer func() {
if r := recover(); r != nil { //відновлення продовження
res = r.(int) //плоди праць
}
}()
res = Fib(n)
return
}
func main() {
fmt.Println(CallFib(10)) //тривіальний виклик
fmt.Println(CallFib(100000)) //Надмірності
fmt.Println("Паніка пригнічена")
}

Спробувати в пісочниці.
Монади в розумінні Haskell процедурному мови просто не потрібні. В Go між тим цілком можна рекурсивні оголошення типів, а багато хто якраз і вважають монади видом структурної рекурсії. Rob Pike запропонував наступне визначення state machine, кінцевого автомата
type stateFn func(Machine) stateFn

де стан це функція машини виробляє дії і повертає новий стан.
Робота такої машини проста
func run(m Machine) {
for state := start; state != nil; {
state = state(m)
}
}

Хіба не нагадує Haskell State Монада.
Напишемо мінімальний парсер, а для чого ж іще потрібні state machine, вибирає числа з вхідного потоку.
type stateFn func(*lexer) stateFn
type lexer struct {
*bufio.Reader //машині потрібна стрічка
}

Нам достатньо лише двох станів
func lexText(l *lexer) stateFn {
for r, _, err := l.ReadRune(); err != io.EOF; r, _, err = l.ReadRune() {
if '0' <= r && r <= '9' { //якщо попалася цифра
l.UnreadRune()
return lexNumber //перехід стану
}
}
return nil // Стоп машина.
}
func lexNumber(l *lexer) stateFn {
var s string
for r, _, err := l.ReadRune(); err != io.EOF; r, _, err = l.ReadRune() {
if '0' > r || r > '9' { //якщо не цифра
num, _ := strconv.Atoi(s)
return lexText //перехід стану
}
s += string®
}
num, _ := strconv.Atoi(s)
return nil // Стоп машина.
}

Працюючий приклад.
package main

import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
)

type stateFn func(*lexer) stateFn

func run(l *lexer) {
for state := lexText; state != nil; {
state = state(l)
}
}

type lexer struct {
*bufio.Reader //машині потрібна стрічка, потік вводу
}

var output = make(chan int) //вихідний потік

func lexText(l *lexer) stateFn {
for r, _, err := l.ReadRune(); err != io.EOF; r, _, err = l.ReadRune() {
if '0' <= r && r <= '9' { //якщо попалася цифра
l.UnreadRune()
return lexNumber //перехід стану
}
}
close(output)
return nil // Стоп машина.
}
func lexNumber(l *lexer) stateFn {
var s string
for r, _, err := l.ReadRune(); err != io.EOF; r, _, err = l.ReadRune() {
if '0' > r || r > '9' {
num, _ := strconv.Atoi(s)
output <- num //передаємо для утилізації
return lexText //перехід стану
}
s += string®
}
num, _ := strconv.Atoi(s)
output <- num
close(output)
return nil // Стоп машина.
}
func main(){
var sum int
a := "hell 3456 fgh 25 fghj 2128506 fgh 77" //приклад введення, для пісочниці просто рядок
fmt.Println("з Числа рядки: ", a)
rr := strings.NewReader(a) //зробимо потік з рядка
lexy := lexer{bufio.NewReader(rr)}
go run(&lexy) //запускаємо лексер окремим потоком
for nums := range output {
fmt.Println(nums)
sum += nums
}
fmt.Println("В сумі дають: ", sum)
}

Спробувати в пісочниці.
Реактивне програмування складно формально описати. Це щось про потоках і сигналах. В Go є те і інше. Стандартна бібліотека io пропонує інтерфейси io.Readerio.Writer мають методи Read() Write() відповідно і досить струнко відображають ідею потоків. Файл та мережеве з'єднання наприклад реалізують обидва інтерфейсу. Використовувати інтерфейси можна безвідносно до джерела даних, скажімо
Decoder = NewDecoder(r io.Reader)
err = Decoder.Decode(Message)

буде узгоджено шифрувати файл або наприклад мережеве з'єднання.
Ідея сигналів втілена в синтаксисі мови. Тип chan (channel) оснащений оператором < — передачі повідомлень, а унікальна конструкція select{ case < — chan} дозволяє вибрати готовий до передачі канал з декількох.
Напишемо зовсім простий міксер потоків.
В якості вхідних потоком візьмемо просто рядка.(Ми домовилися робити приклади негайно виконуються в пісочниці, що обмежує у виборі. Читати мережевого з'єднання було б цікавіше. Код може практично без змін.)
reader1 := strings.NewReader("ла ла ла ла ла ла ла")
reader2 := strings.NewReader("фа фа фа фа фа фа фа")

Вихідним приймемо стандартний потік виводу
writer := os.Stdout

В якості керуючих сигналів використовуємо канал таймера.
stop := time.After(10000 * time.Millisecond)
tick := time.Tick(150 * time.Millisecond)
tack := time.Tick(200 * time.Millisecond)

І весь наш міксер
select {
case <-tick:
io.CopyN(writer, reader1, 5)
case <-tack:
io.CopyN(writer, reader2, 5)
case <-stop:
return
}

Працюючий приклад.
package main

import (
"io"
"os"
"strings"
"time"
)

func main() {
stop := time.After(10000 * time.Millisecond)
tick := time.Tick(150 * time.Millisecond)
tack := time.Tick(200 * time.Millisecond)

reader1 := strings.NewReader("ла ла ла ла ла ла ла")
reader2 := strings.NewReader("фа фа фа фа фа фа фа")
writer := os.Stdout
for {
select {
case <-tick:
io.CopyN(writer, reader1, 5)
case <-tack:
io.CopyN(writer, reader2, 5)
case <-stop:
return
}

}
}

Спробувати в пісочниці.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.