Golang Functional Programming (and why you should NOT do it)
Вставка
- Опубліковано 6 лип 2024
- Golang Functional Programming (and why you should NOT do it)
Did you know we already have functional programming support in Golang? In this video, I'll talk about how you can start writing functional code in Go and some benchmarks on functional Go.
Should you do it? Watch and let's find out. Enjoy!
--
Golang Dojo is all about becoming Golang Ninjas together. You can expect all kinds of Golang tutorials, news, tips & tricks, and my daily struggles as a Golang developer. Make sure to subscribe if you look forward to such content!
Get Your Golang Cheat Sheet! - golangdojo.com/cheatsheet
Git repos & notes - golangdojo.com/resources
Golang Releases - • Golang 1.18 Overview (...
Golang Informative - • How much do Golang dev...
--
Timestamps
0:00 Intro
1:08 Filter method
2:05 Filter positive function
6:30 Stream struct
10:43 Benchmarking
12:29 Outro
--
#golang #goprogramming #golangdojo - Наука та технологія
📝Get your *FREE Golang Cheat Sheet* -
golangdojo.com/cheatsheet
PS - Video idea accredited to Amir from our secret Discord server. Find out how to join by signing up for our newsletter!
HELP NEEDED: I have tried multiple emails and I haven't received my cheatsheet yet
i would say that allowing != supporting
from what I know, in Java, stream methods are smart: you tell them to filter this, map this, reduce that, but they do absolutely nothing... until you tell them to fire at the end.
only then your whole chained instruction is being read an analysed and compiled to an optimized version like the one that you showed in Go in the end.
that is the whole idea: you tell me what you want, and let me worry how we will get there as fast and as efficient as possible.
functional programming can be unbelievably productive, safe, and fast too, but it needs to be natively rooted into the language itself, otherwise it becomes useless.
but maybe that is ok, not every language has to be functional, and go can keep embracing its imperative nature of explicit looping and branching. i still love it! 💙
I have two comments here, first is that like others have suggested, there is a difference when running two filter operations than just a single operation that does both checks. Here are results of doing so, where a single filter is nearly as fast as a single loop:
BenchmarkFPTwoCalls-8 794659 1440 ns/op 128 B/op 5 allocs/op
BenchmarkFPOneCall-8 5266573 215.9 ns/op 8 B/op 1 allocs/op
BenchmarkTwoLoops-8 1000000 1291 ns/op 128 B/op 5 allocs/op
BenchmarkOneLoop-8 7161225 149.1 ns/op 8 B/op 1 allocs/op
I'm running this on a fairly slow ARM chip (a chromeOS tablet) so the numbers are different but the like the original 2-filter / 2-loop runs, the 1-filter/1-loop versions are similar. I suspect one difference in both cases is less the generics, and more the fact that function calls (which are likely not inlined) are slower than comparison instructions, although still quite competitive.
Second comment:
A big difference between 2 -> 1 steps is likely the memory use (as shown by the columns added by --benchmem), as the two-pass versions must (re)allocate memory several times because they need to grow slices several times (mostly during the isPositive step). Different data, especially one where the final results have significantly more than a single result may show less of an advantage. Here is the results with an input array of a million random integers:
BenchmarkFPTwoCalls-8 9 134851111 ns/op 62761522 B/op 73 allocs/op
BenchmarkFPOneCall-8 25 50334985 ns/op 21083401 B/op 35 allocs/op
BenchmarkTwoLoops-8 14 85377324 ns/op 62761466 B/op 73 allocs/op
BenchmarkOneLoop-8 31 40581509 ns/op 21083400 B/op 35 allocs/op
The benefit of one step now is now much closer to a 2x speedup, which does line up with the allocations. If you were to have Filter pre-allocate the output slice so it only needs a single allocation, you may have wasted space in the slice, but actually less garbage due to not throwing away the old slices, and everything speeds up ~2x again.
BenchmarkFPTwoCalls-8 14 87367445 ns/op 16007175 B/op 2 allocs/op
BenchmarkFPOneCall-8 48 26948692 ns/op 8003588 B/op 1 allocs/op
BenchmarkTwoLoops-8 45 28888468 ns/op 16007172 B/op 2 allocs/op
BenchmarkOneLoop-8 68 17966153 ns/op 8003588 B/op 1 allocs/op
Essentially, be careful with benchmarks and what you are actually measuring, as here a non-trivial portion of the time (half of it or more) was simply from allocations / GC on top of of the choice of filtering method.
One question why did you not benchmark a isEvenAndPositive solution to have a more closer test to your one loop solution?
I'd like you to dig a little deeper into GoLang's type system. Here's something that's achievable in GoLang's type system but I found this out by trial and error and not by reading any docs. Basically I created a type that allows me to create a function which can return the current answer and a function closure which can be called to return the next value and a function closure... This is a basic example of lazily evaluating a fibonacci sequence. This is where the type system really gets interesting.
package main
import (
"fmt"
)
type Func func() (int, int, Func)
func (f Func) call() (int, int, Func) {
return f()
}
func fib_aux(x int) int {
if x == 0 {
return 0
} else if x == 1 {
return 1
} else {
return fib_aux(x-1) + fib_aux(x-2)
}
}
func fib(x int) (int, int, Func) {
return x, fib_aux(x), Func(func() (int, int, Func) { return fib(x + 1) })
}
func main() {
d1, n1, f1 := fib(15)
d2, n2, f2 := f1.call()
d3, n3, _ := f2.call()
fmt.Println(d1, n1)
fmt.Println(d2, n2)
fmt.Println(d3, n3)
}
This feels fundamentally wrong, a true stream would be much faster. This doesn't operate in the same way the Java streams API does, this has lots of blocking and waiting for things to happen, in order to create something like this you should implement with channels chaining the functions together so you can actually have a single stream of data flowing through all the methods, having loops and waiting for one section to finish before starting the next is missing the point of what a stream is.
Also Golang doesn't need lambda functions as it has first class functions, lambda is a bit of a hack in Java to help achieve a similar goal
With generics and built-in support for higher order functions, you should be able to make a workable Optional[T any] type.
Actually I just made a lazy generic seq with GoLang.
js is full of those array methods. But for rang iterator is good in go. I’am from JS and was curious of those things.
I’m a new subscriber and I must say you are doing a great job with your demonstrations. Thank you very much 😃
It looks to me the video should've been titled "Golang Functional Programming (how and WHEN you should not do it).😀It is a very nice feature combined with generics as long as you are not constrained by performance requirements.
Well even in functional languages you have to think things over. Its logical that if you loop through an array twice it would take longer then doing it once. Create a funtion that does 2 things at the same time in fp would also go faster. This not a good example.
awesome
this just sounds like a reason not to use Go
Golang designers have worked hard to create a language with the largest possible number of reason not to use it.
@@michaelmroz7433 ahah indeed
You can create this example in any language. Bad programming is not the fault of the language
@@michaelmroz7433 🔥
Wrong comparison with the last benchmark. One can easily write a function that does it (isPositiveEven()), looping through the slice only once.
lambda functions are indeed in go.
example here:
func run[T any](a T, f func(T) T) T {
return f(a)
}
func main() {
fmt.Printf("%d
", run(4, func(i int) int { return i }))
}
your implementation don't match java/c# implementation. stream is not supposed to do multiple loops, it's lazy calculation, the function will be called when / at the iteration time.
Functional programming isn't just about using hifgher order function! It's also about using design patterns like Monoids, Monads, Functors, Applicatives and so on. If you add them properly, say state monad, it can eliminate if err != nil statements completely from your program. That's where the power of functional programming lies. Is correct and readable code, better than code with little marginal performance hit? Absolutely!! Because code is written once, and read a million times. One optimization of the compiler down the line, these problems would be non existent. Heck go is even inspired by functional patterns such as error as value, or channels.
Go lang designers : let's make something people are not familiar with, weird and make them cringe, but we will work hard to make the language very fast just to trap them and torture them, they will probably make a a youtube channel about our language.😂
Sounds like you're new at programming, have you ever heard about C? 😅
Golang wants you to be simple, no complicated sh*t, just be the simplest you can be ..... ☮
filter is not simple?
Map, filter and even reduce easier/simper/readable than loop.
Many things are much simpler than imperative code with mutable state.
Unfortunately. Funcional make more simple and safe
@ThatGuyJamal I'm saying this after years of OOP and 2 year of Elixir to see how different and easy is functional and how much problem OOP create to itself to try to solve with some crazy technique that don't need in a functional paradigm
@@MarcosVMSoares this is not OOP v functional this is iterative v functional.
totally agree, it's crazy that golang dev need to write the same thing over and over, in elixir, you can just Enum.filter(&(&1 > 0 and is_even(&1)))
@ThatGuyJamal Writing the same thing over and over is pretty bad for productivity, i think golang team can make a optimazated inline function.
var filteredList [ ]int
for _,value:= range(list) {
if (value > 0 && value % 0 == 0 {
filteredList = append(filteredList , value)
}