Is it possible to write games like Pac-Man in a functional language? (2008)
prog21.dadgum.com
prog21.dadgum.com
new_game_state = simulate_a_frame(old_game_state, user_input, time_delta)
So of course it's possible. Whether it's practical is another question.
This abstraction was almost fully stripped away by the compiler. Since the original Machine object is no longer required when the step function returns from executing an instruction, and since the size of the emulated RAM never changes, the compiler can, for example, represent the list of RAM values as an array, and mutate the array in place and internally just keep track of the pointer to the mutated object and return that. It isn't necessary to copy most of the data structure most of the time.
minihs keeps itself small, uses a stack of copies of source to implement []. Whereas the larger version parses the source to a vector of opcodes (which collapses repeated +-<>) & includes a jump table
Although in practice "purely functional" programming is a bit silly here. The point of PacMan is all the IO which is fundamentally all side effects. There are a lot of practical compromises that would be called "functional" like storing all the major state of the game in a big array (something like [turn1, turn2, turn3, ...]) and then implement a turnX -> turnX+1 animation function.
The fundamental insight of the functional approach is implementing a frame->frame function was already something that implicitly had to be done to make the game. If it is an explicit function then debugging it is easier. Because the input can be saved. No extra work is needed.
In fact, the point of a huge proportion of programming is IO and is "all side effects." It's still possible to adopt a functional style for internal calculations (and in fact this helps to decouple components and makes testing much easier, at the expense of relatively negligible copying overhead, so it's worth doing) but once you get beyond your internal toolkit and start actually trying to test the whole system, you're interacting with the outside world.
To be sure, I don't think this is a correct interpretation, but I do think it may play a role in the difficulty of getting people onboard with FP.
It's the opposite, for FP, OOP, imperative, that is the part of the program which is easiest.
FP is about managing the difficult parts, and so is OOP. They just have different approaches, and note, many mainstream languages are incorporating FP concepts quite readily now.
It's also not just a syntax difference, mainstream languages are embracing immutability more and more, often as a default.
The point is that calling FP academic is a tired trope and even if it is far from the dominant paradigm, there is clearly practical lessons to be learned whatever paradigm you program in.
A Haskell program covered in piles of monads to abstract all state away still follows the functional core imperative shell pattern, because the runtime is the imperative shell. This doesn't make such a thing desirable for other languages.
The practical lesson I learned from dabbling with functional programming in academia is that functional programming is often impractical and does not offer any of the purported benefits of better code.
Most mainstream languages are old, they can't be immutable my default without breaking backwards compatibility. I'm talking about the design direction of languages that are already based on mutability. For example, Java added Records which are immutable, as well as overhauling the date/time api with an immutable version.
> This doesn't make such a thing desirable for other languages.
I've used this pattern in other languages, none of this is a silver bullet that makes software engineering easy; but I do find it makes some code much easier to test and reuse.
A shell is not more imperative than a processor - both are Turing machines, a completely functional mathematical object. Seeing them as imperative is a semantic interpretation of the people watching it, not an inherent property.
This is what state monads are doing anyway under a hidden syntax, and for simple cases it may be easier that building a full dedicated monad.
Some languages make this easier to model than others, and some make doing so in a pure style easier, eliding real-world side effects like RAM access, GPU, etc.
Once you are comfortable with this concept, functional languages make a lot more sense, if you don't get lost in symbol soup.
Edit: a simple Google search for "functional core imperative shell" brings up many writings on the concept.
Input is an obvious example, as it's impossible to predict the value of an input.
Output is less obvious, but there is uncertainty like whether the filesystem is full.
The original Haskell style was to have a World object that was an input to every non-pure function.
Do you mean this more abstractly or literally? In the example of PacMan, for every frame, the only possible input is nothing, left, right, up, or down. So you are be able to predict it is going to be one of those five, though not which one.
If you model the controller input as a function parameter, that's just a state argument, and FP can handle it declaratively just fine without having side effects.
>> Input is an obvious example, as it's impossible to predict the value of an input.
> though not which one
Literally.
So because the possible input is not just a single value, it is not predictable. It doesn’t matter if the input was limited to a set of two values, or infinite, it is literally not predictable so therefore we don’t know the future state of the program?
"Effects" (without the "side") usually means the effect is modeled as a type/value that is returned by the function.
Side effects are the opposite of pure functions. A function with side effects will mutate global state, a function without side effects will not (it will simply return a value).
https://en.m.wikipedia.org/wiki/Side_effect_(computer_scienc...
But "imperative" has such bad mouthfeel that I had to swear it off altogether.
Most modern PC games/engines have a really good instant quicksave system which dumps the entire world state very quickly.
I confess that sometimes things do, in fact, get a ton easier if you expand your abstraction out such that you are abstracting the whole board, and not a bunch of individual pieces. That comes with its own set of road blocks, though. In particular, game worlds are a lot bigger now than they used to be.
How far away are we from being able to represent the GPU state and rendering instructions with simple datastructures ready to be submitted wholesale to the driver?
And then render based on delta and interpolate movements.
Using a time delta would give different results when doing non-linear stuff. Like applying forces. It's different to apply "force * dt" vs doing "force * 0.5dt" twice. Also if you use a big dt you could end up going through walls or similar. So therefore the physics engine should run at defined ticks in most instances, or it will behave differently based on framerate.
And I can’t think of any reason why you’d combine them.
Recording it is still command driven but trying to give an entire app scene in one go is really messy. We tried this internally on our gpu before Vulkan.
I imagine that comes down to how smart the compiler is.
[1] https://wiki.haskell.org/Frag
[2] https://wiki.haskell.org/Applications_and_libraries/Games
I recently hacked together an asteroids clone in Haskell with SDL2 and not much else. It’s not super pretty but it works.
I’ve talked to folks who’ve been using the newer effects libraries taking advantage of the new delimited continuation primops in GHC 9.6 for their game dev. Even with very high level libraries their reporting acceptable performance.
With enough dedication I’m certain anyone could make whatever game they wanted in an FP language.
> I've spent enough time with functional languages to have figured out how to implement non-trivial, interactive applications like video games. My plan is to cover this information in a short series of entries.
The biggest problem with the approach is that mainstream functional languages put most effort in the functional core rather than the imperative shell modeling.
Effect systems (ZIO for Scala, effect-ts) are the only places where fibers allow composing, scheduling and managing "processes" in a functional way.
I think questions like this are really getting at, is how do you update the 'huge-game-state' without mutating it, changing variables.
People have a very ingrained feeling that the game state is updated by updating variables.
But in functional languages, the state is immutable, so if you are new to functional languages you ask 'man how do I update this'.
Then, if this new person takes next step and is told 'you make a copy of the game state', they are like 'man that must be a big performance hit to copy the whole thing'.
So I think questions like this are really about how to handle a state with immutable data. How to make a copy efficiently, like in parts, or whatever. And then of course, you are down the rabbit whole of changing your brain thinking towards functional style.
I had the luck of having a whole semester doing just that with various functional and logic languages (Haskell and Prolog) with a great teacher.
You start by learning accumulator parameters and tail recursion, then you build and learn to use foldl and lazy functions; and then you may learn monads as a design pattern to reduce boilerplate for those techniques.
Sure you can create good programs without them, and the first developers often did; but nowadays a good programmer is expected to understand why those techniques are relevant and what kind of problems appear if you decide to ignore them on purpose.
For complex modern programs, specially on the web, the 'pure functional core with iterative shell' (possibly imperative, but it can also be funcional reactive) is an increasingly common architecture that the most popular frameworks are converging to.
The ills of goto and poor scoping were solved with compiler rules and very simple expressions that could be added to any language. The alleged ills of mutation as described by academic groups made up of mathematicians whose jobs very infrequently have to do with creating actually useful software often do not match the reality of coding, nor have they ever been so bad as to demand alternative solutions. The vast majority of languages had no problem eliminating GOTO; that only a small fraction have implemented purity puts it on the same level as hardcore OO, which also is only a small fraction of the market, and about as useful.
All abstractions introduce complexity in terms of indirection and the need to particularise to different instances. Imperative and functional simply happen to have a different approach to where they place complexity and what parts they simplify.
Imperative makes it easy to update state, but then it forces you to keep in your mind every remote part of the program where your symbols may be modified (which is way harder than most developers recognize). Functional requires more work to keep track of state, but on the other hand it allows much better control of program composition and deep complex hierarchical data transformations.
As they say, use the best tool for each job. It makes no sense to deride a key wrench for being more complex than a hammer and being worse at hitting nails.
>Imperative makes it easy to update state, but then it forces you to keep in your mind every remote part of the program where your symbols may be modified (which is way harder than most developers recognize)
Scoping inherently reduces the extent to which state is considered. Any imperative programmer worth their salt knows to minimize reliance on global variables - functional programming salesmen claiming a significant advantage by eliminating bugs involving them can't help but come off as more than a little condescending by belaboring this point.
>As they say, use the best tool for each job.
This is a big step down from "All programmers should know and implement purity by default because mutability is the next goto."
Not something I said. I explicitly mentioned the functional core, iterative shell architecture, and explained how you need to understand functional enough to know when it's a good time to not use it.
At this point your posts look like someone making fun of a complex technology they do not understand, without really approaching any valid criticism of its real shortcomings, because such criticism require a thorough understanding of the thing being criticised.
> Functional programming that introduces or models state requires more abstraction, and then complexity.
I'm pretty sure I've said exactly that. Good thing we agree on it. But you don't seem to realize that this fact is only true for handling state, and not for other aspects of programming.
> Imperative programming with state is as simple as functional programming without state.
You've never tried to compose multiple asynchronous event streams of complex data types from different subsystems into one single user-facing unified presentation, have you? Your assertion is simply not true. Such feat is way simpler in functional reactive style, and a true nightmare in an imperative multithreaded program. That's why web frameworks are evolving to handle more and more reactive functional patterns for compositional asynchronous tasks.
>You've never tried to compose multiple asynchronous event streams of complex data types from different subsystems into one single user-facing unified presentation, have you? Your assertion is simply not true. Such feat is way simpler in functional reactive style, and a true nightmare in an imperative multithreaded program. That's why web frameworks are evolving to handle more and more reactive functional patterns for compositional asynchronous tasks.
Imperative programming with state is simple. Programming asychronous event streams of complex data types from different subsystems into a single unified presentation is complex. Functional programming claims to have abstractions that make the latter more simple than imperative programming, which may or may not be true.
The use of functional reactive style in React (I do not believe Angular relies on this) is probably driven by JavaScript having atrocious scoping rules and the general Web / DOM stack being full of hacks and badly written APIs. The hacks that are still present in React make it seem like this will just be yet another layer of cruft that is the front end, alas.
Then you missed the point, which is that using mutation a.k.a. cells a.k.a. imperative code when it's not needed is a dinosaur to be culled. But you need to understand when it's not needed to realize this.
Functional reactive programming adoption is not something caused by the state of one particular stack; as it is also being adopted by game engines, big data analysis tools, development environments like web notebooks, etc. People throughout the industry are seeing the value and are simplifying their tools thanks to the paradigm.
> Functional programming claims to have abstractions that make the latter more simple than imperative programming, which may or may not be true.
Look, I get it, I really do. You don't see the possibilities of the functional paradigm and don't know how and where to use it effectively, so you keep criticizing the small part of it you do understand. I've been there, done that (though it was a long time ago).
With that attitude you can stay comfortably in a corner and be impervious to the changes the industry is undergoing, and miss out on a really cool programming style out of prejudice. Or you can accept the intellectual challenge (which it certainly is, especially for someone who has spent his entire career with a single style), and know concepts that those who learn them agree that they improve their programming even if they do not adopt them 100% for their work.
It's up to you. I don't gain anything with this, so I will not try to convince you of it.
> I'm pretty sure I've said exactly that. Good thing we agree on it.
I strongly disagree with the complexity part of this statement. Yes, FP is in many ways "more abstract", but so is natural human thought. More abstraction is not the same as more complexity (except when the abstractions are inappropriate in a given situation), does not lead to more complexity, and the FP approach to state is objectively simpler in most* real-life scenarios.
[*] as always, exceptions do apply.
This is very handwavy, because i’m speaking in very general terms. But for a single, narrow, non-general, concrete example, just look at the ORM impedence mismatch problem with OOP languages. AFAIK, after 40-some years it’s still unsolved. I don’t believe FP suffers from this specific problem (again, just to give a concrete and non-handwavy example), because FP is very analogous to relational programming (functions are a special case of relations).
This shouldn’t be so controversial and should be very intuitive to anyone who spends a bit of time thinking about it. A function is fundamentally a simpler building block than a class. It’s more atomic.
The cost of implementing everything as a function is when one has to contend with the fundamental machine architecture not dealing with everything as a function. This often leads to one of the highest cost abstractions out there, the Monad.
x + 1
Moreover, this example is totally meaningless, because it doesn't feature anything that is different about FP and the imperative style at all.No it's not, objectively speaking. You're confusing simplicity with familiarity.
If you had to build the whole runtime yourself from first principles, functional behaviour would be way simpler to define.
Basically, all you do is define a function (fun (arg1, arg...) -> result) and invoke it (the exact same way you define a callback with .map(() => result) in Javascript for example).
You can store the local state of the function you're currently in, into the callback and state can be passed around that way. (Say you are in a JS function where you defined a number or other variable above: you can use that number in any continuation/closure/function and pass it around, to .map() for example or another function.)
I've also heard of recursion as being a way to pass state around in a functional language. If you have a recursive function that takes a num parameter and calls itself with (num + 1), the number can be viewed as mutable state written in a not-so-mutable way.
This means any functional programming language can implement any imperative programming language with a factor of O(log(n)) overhead.
Where you represent the composite structure hanging from the point you want to mutate, so updating that point is merely dropping the header and appending the updated value as a new prefix.
Suppose you have array mutation, but the changes are only local.
Then as TuringTest pointed out, there are efficient ways to make those local changes in a functional programming language without the log(N) overhead.
[2] https://github.com/fable-compiler/repl/blob/master/public/sa...
It's not better than using C, but worked just fine when I wrote it in 2014. I'd say, unless you're planning to sell it, write the game in whatever language you enjoy.
Part 2: https://prog21.dadgum.com/24.html
Part 3: https://prog21.dadgum.com/25.html
Part 4: https://prog21.dadgum.com/26.html
Follow up: https://prog21.dadgum.com/37.html
- "Not being able to re-use names for values in functions". It's not a feature of functional languages, and thankfully many these days don't suffer from this limitation (see Erlang vs Elixir for example)
- "The lack of a simple dictionary type". Again, blame the terrible functional ecosystems of 2008.
About ergonomics, I think it’s a matter of opinion. To me though it is very ergonomic. As an example, let’s say you get the users input, trim the length, and parse it as a number. Without variable shadowing you have 3 variables (like, inputResult, inputStr, inputNum) and all those variables are still accessible , potentially by accident, throughout your code.
If you just have one input variable that you redefine, then you don’t have to pollute the scope with variables or think of more names and you won’t accidentally use one of the intermediate ones elsewhere. To me that’s more ergonomic.
This is just redeclaring or redefining the variable.
Variable shadowing is allowing a variable of the same name as an existing variable, but within a different scope.
The difference in scope is absolutely critical - because the shadowing happens within some scopes, but not within others - there are still two instances of the variable, and both values are still kept, but which one is visible depends on the scope.
Basically, to quote the def - "At the level of identifiers (names, rather than variables), this is known as name masking. This outer variable is said to be shadowed by the inner variable, while the inner identifier is said to mask the outer identifier."
Then this is just a flavor that you haven't seen. https://doc.rust-lang.org/book/ch03-01-variables-and-mutabil...
> Variable shadowing is allowing a variable of the same name as an existing variable, but within a different scope.
By different scope you mean there's an inner scope and then there's an enclosing scope, where the inner scope has access to the outer scope but not vice versa. This (Rust's shadowing) more or less works and feels the same, but the difference is that additionally allows variable shadowing to be done within the same scope.
> The difference in scope is absolutely critical
I would agree with you if the way that Rust implements shadowing took away the ability to do what you're talking about, but that remains in place. You just have the extra option of redefining an immutable variable within the same scope as well, so it's purely additive.
Personally I don't think there's anything bad with variable shadowing within the same scope, as at times it can be useful and help avoid mistakes. It's like, there are times when you are fine with original variable never coming back into scope, and you now have the ability to do that. I don't see any problems or dangers from that, especially if normal closure/block scope variable shadowing remains.
A shadow is obscuring an object that is still very present, just hidden. That object can be seen again by changing scope.
If there's no way to get back to the original (or the only way back is through a different name), it's not a shadow, just a redefinition. That name doesn't have a shadowed copy, it just has a previous value.
Nothing wrong with redefining a variable either, it's perfectly valid. It's just different than shadowing, in my opinion.
Every line of code starts a new scope.
Scope is not about lines, it's about visibility.
Is that how other languages are starting to do this?
And agreed that not polluting the scope is good. That is why mutating the variable was the preferred way for a lot of folks.
I can’t say they were wrong
https://github.com/opyate/minild72/blob/master/src/pong/core...
The simple counterexample, take setpixel, which takes an image buffer, and returns an image buffer with one pixel changed. Now imagine having a large bitmap and using setpixel to draw a sprite to it. For every non-transparent pixel in the sprite you need a whole copy of the whole buffer. Obvs there are lots of improvements and cleverness but compared to the speed and simplicity of an imperative code just modifying an image buffer in-place it's vastly slower, more complicated or both.
However you can manage to write "most" of your game in a functional way and if you're careful about it, and use tricks like copy-on-write to make some things act functional, you can do it. Writing your game in a functional mindset, and keeping data immutable when you can has a lot of benefits.
- https://github.com/EgorDm/fp-pacman (https://www.youtube.com/watch?v=DlifcJ4cexw)
- https://github.com/Jcklly/PacMan
- https://github.com/ajkavanagh/pacman
and other games:
- https://wiki.haskell.org/Applications_and_libraries/Games
See also the #haskell-game chat on Matrix and Libera IRC.
See a few (small, demo) games built by the community in [2] .
Notice Elm has abandoned the FRP approach in favor of Model-View-Update [3].
[1] https://elm-lang.org/ [2] https://github.com/rofrol/elm-games [3] https://elm-lang.org/news/farewell-to-frp
Not pretty, but those CPUs were designed to be really cheap.
> Before I studied the art, a binding was just a binding.
After I learned the art, an assignment was an assignment but an application was an application.
Now that I've understood the art, a binding is just a binding.
16 bit addresses didn’t fit into registers, so you keep pointers in the zero page (bytes 0-255). There was no 16 bit increment either, so to increment the pointer you incremented the low byte and on overflow increment the high byte
Non-constant modulo can often be computed by checking for roll-over when incrementing (or just computed in a similar way to division)
If you think about it, 1.) most performance comes down to memory access. 2.) It is widely known that accessing memory off the stack is faster than allocating memory on the heap. 3.) calling a function pushes everything onto the stack, starts a new stack pointer, and when it’s done releases everything from the stack.
So my thinking is if by definition functional programming is about organizing your program as a pure composition of functions, doesn’t that also imply an ideal memory access pattern? Can someone tell me where I'm wrong?
I guess the one issue where this comes into doubt is with strings of unknown length, and other data types that must be heap allocated, but even in those cases it seems that problem would equally affect all other paradigms.
ps: of course FP shields you from having to manage correct state update seen in imperative programming, which might require a lot, a lot, of reads and writes to ensure things are not messed up.
This is even harder if you work with data structures like threaded trees, where there are so many links that even the zipper approach basically falls flat. (And the zipper already has performance questions if you do a deep edit, if I am not mistaken. What could be a single value mutation is now a rewiring of every link down to the value that you wanted to change.)
This is a very C-centric view of it.
In FP, sometimes they're functions, sometimes they're closures. Sometimes they're fully applied and sometimes they're partially applied.
You're probably on the heap, not the stack for two reasons: tail recursion, and variables very often outliving the return.
I mean like… what’s stopping you?