Насколько объектно Go ориентирован многократно и эмоционально обсуждалось. Попробуем теперь оценить насколько он функционален. Заметим сразу, оптимизацию хвостовой рекурсии компилятор не делает. Почему бы? «Это не нужно в языке с циклами. Когда программист пишет рекурсивный код, он хочет представлять стек вызовов или он пишет цикл.» — замечает в переписке Russ Cox. В языке зато есть полноценные lambda, closure, рекурсивные типы и ряд особенностей. Попробуем их применить функциональным манером. Примеры покажутся синтетическими оттого, что во первых написаны немедленно исполняемыми в песочнице и написаны на процедурном все же языке во вторых. Предполагается знакомство как с Go так и с функциональным программированием, разъяснений мало но код комментирован.
Closure, замыкание реализовано в языке в классической и полной мере.
Например ленивую рекурсивную последовательность можно получить так
func produce(source int, permutation func(int) int) func() int { return func() int { //первоклассная lambda source = permutation(source) //замыкание source return source } }
Простой вариатор для псевдослучайных чисел
func mutate(j int) int { return (1664525*j + 1013904223) % 2147483647 }
И вот наш генератор случайных чисел
next := produce(1, mutate) next()
package main import ( "fmt" ) func produce(source int, permutation func(int) int) func() int { return func() int { //первоклассная lambda source = permutation(source) //замыкание source return source } } //простой вариатор для псевдослучайных чисел func mutate(j int) 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()) *Timer принимает аргументом 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("Hello") //экземпляр типа 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 max int = 200 func CallFib(n int) (res int) { defer func() { if r := recover(); r != nil { //восстановление продолжения res = r.(int) //плоды трудов } }() res = Fib(n) return }
package main import "fmt" var max int = 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(n int) (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 Monad.
Напишем минимальный парсер, а для чего же еще нужны 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(r) } num, _ := strconv.Atoi(s) fmt.Println("there", num) 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(r) } 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.Reader и io.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 } } }
ссылка на оригинал статьи https://habrahabr.ru/post/280210/
Добавить комментарий