F# Good and Bad
Code stupid. Clean smart.
I like F# because it's the best programming training language I've found. There are probably better, and as NET evolves F# is getting shakier and more byzantine, but it still works great for making me think about and become a better programmer, no matter what language I use in everyday work.
That means that when I step away from the language and come back, I learn something about the way I think. I can also see this in others; people grasp to turn their thoughts into code. It's fascinating to watch the struggle, both in myself and others.
My net has been out, but it came back up yesterday. To relax I decided to start on the Advent of Code 2021. It's a website that gives you a couple of problems for each day in December up to Christmas Day. You can solve them any way you want. Because of that it's become a central place in December for coders all over the world to play at solving sample problems. Think of it as Sudoku for nerds. I'm not sure there's anything you get out of it aside from the dopamine hit of hey, that's pretty neat. I explained it to my SO and she shook her head sadly. Most folks do not get it.
Today I dropped by the Reddit F# forum and sure enough, there were some posts with programmers struggling. I found the advice given both good and bad at the same time, and that inner conflict fascinated me.
Questions That Have Opposite Answers (and why)
Have I typed this code in right?
In any programming language, there are a lot of different ways to do the same thing. Do you want to use Int32.Parse(num) or (int)num? Do you want to pass an unmodified single-argument function into a lambda or just collapse it all, e.g. List.map (fun d -> Int32.Parse(d))
or List.map int
?
F# is cool ecause these questions of code collapse go on and on. Eventually you end up in point-free world, where foo|>bar|>baz and so on. So really this is a question of what should you clean up, when should you clean up, and why should you clean up?
I've found that this is best done once you have pure functional code. Mutability and options confuse and muddy up everything. You can most always get rid of them. Once you do, you'll have a hell of a fun time collapsing all of this down to a point-free masterpiece. (To me it's a better feeling that doing AOC because you're working on real-world problems). This advice is right, and you're welcome to learn and use these kinds of simplicities any time you'd like. This advice is wrong; you probably shouldn't be shining up the wheels on the fire truck when there's a house on fire. The advice is good and bad.
You want your F# code to be the performant code and use OO languages like C# code for your real work, right?
Microsoft is responsible for this bullshit. They wanted a way to advertise F# to their existing codebase, and somehow when they cranked the marketing sausage-making machine, "F# is great for specialized problems!" came out. We do "special" problems: performant, scientific, integral calculus. That kind of thing.
This is good advice because if you want performant code, go pure functional and then you get distributed processing and scaling for free. Object-oriented code tends to hide what you're doing behind abstractions for other types of problems that the library creators had in mind when they made the library. With F# (and other functional languages) you're on your own. This should "make" you code a different way. (Sadly, it does not, and mixing up programming language paradigms is the end of Western Civilization, but that's a topic for another day).
This is bad advice because unless you know the rest of the story, you really don't get anything than you had before, only now you've scattered your solution into even more buckets and languages.
You want to use $IDE/TOOLS here
Programming, the language, the tools, the community, the rest of it – it's an environment where you either become a better coder over time or a worse one. Have a lot of bugs? Do a lot of code maintenance (not cleanup) that doesn't involve new features? You're going down the wrong path. The rest of that doesn't matter. At all. You can code everything you want in Visual Sumerian or Reverse Klingon– for all I care.
It's good because good tools matter. It's bad because you gotta struggle through this yourself. If one set of stuff worked for everybody, robots would be doing your job. Robots are not doing your job.
I'm using AOC and F# to learn $X
Yay! None of this applies to you. Learning means taking a deep dive along one topic. Making useful stuff means looking at end value and then taking the simplest, most maintainable path to delivering that value. These two concepts are mutually orthogonal, although focusing on one of them can make the other better.
Okay, Wise Guy, What's Your Answer?
My code sucks and I wouldn't post it in a forum. I'd be afraid others would copy it.
Here's some from Day 2:
let countOnesInABitPosition (n:int) (stringList:string list)=stringList|>List.sumBy(fun x->if checkIfOneIsAtIndex x n then 1 else 0);;
let countZeroesInABitPosition (n:int) (stringList:string list)=stringList|>List.sumBy(fun x->if checkIfOneIsAtIndex x n then 0 else 1);;
let mostPopularBitForAPosition (n:int) (stringList:string list) =
if (countOnesInABitPosition n stringList)>(countZeroesInABitPosition n stringList) then 1 else 0;;
let leastPopularBitForAPosition (n:int) (stringList:string list) =
if (countOnesInABitPosition n stringList)>(countZeroesInABitPosition n stringList) then 0 else 1;;
let getMostPopBits(stringList: string list):int [] =
stringList.[0].ToCharArray()|>Array.mapi(fun i x->(mostPopularBitForAPosition i stringList ));;
I could go on for a very long time about all of the problems here. We've got strong typing where it's not needed, huge functions with single-variable lambdas that aren't collapsed. My critique goes on and on. I suck at writing really cool code when I'm cold and I first sit down with my tools to code. As I've gotten older the problem has just gotten worse. This has taught me more about teaching and understanding programming than decades of actually teaching programming has.
The point here is about how to get to the goal of excellent code, not what your excellent code looks like.
This is my main problem with coding advice, including my own: it confuses the two domains in the mind of the person trying to learn. Learn something or solve something. If you're learning, it doesn't matter whether you end up doing anything useful or not. Most times not. If you're solving, none of this bullshit matters at all. Instead you've got to get good and making the benefit force itself out of the tech in the simplest, most maintainable fashion possible.
In that manner, when I look at code like mine above, I've put in all of this bad-code noise so that I can understand and organize the solution to the problem. It passes the acceptance tests and only those tests. [1] Nothing outside is affected. If I come back in a month and can read the code, if a new person to F# can read the code? I'm rocking the problem-solving domain.
Compare that code to the main code to a compiler framework I created a few years ago: [2]
There's nothing here to fix. It's appropriately-typed, it's point-free, tacit, and composable. It's pure functional code and it reads well in English. This is the kind of code I tend to post (and read) online. I love code like this. I love writing it and I love reading how others code this way.
Here's a middle-of-the-road example from an analysis compiler I was developing back in 2016:
This is also noisy. There's assignments, big-honking catch-all try/with blocks. Hell, there's even printfs in there. That's the thing you never do!
The first example was a complete mess of code. The second example is code that I consider, for lack of a better word, beautiful. This last example looks confused to me. I can see what's going on, but all of those blocks and printfs, along with other factors, make me leery of praising this too much. I can see I'm going to have to come back here.
All of these code examples pass their acceptance tests. None of these are broken from the standpoint of the outside caller or end-user. So if you want to talk style or idiomatic code, the middle one is coded in the most popular way. The other two are not.
But none of this is right or wrong. The code passes its tests, end of story. What we're looking at here in the difference in coding constructs represents how the mind of the programmer is beginning to understand the actual problem to be solved. The Advent of Code example has all kinds of huge variable names and step-by-step coding because inside my mind I'm thrashing through just what the hell this guy is wanting me to solve, not how to program in F#. I already know that. By the same token, the second example represents a domain where I know exactly what the business problem is. I don't expect to have to think about this again.
The "coolness" of the functional code represents the inner programming process. You can't evaluate it on its own. The AOC code I might flail around for a while getting to work. I might have to come back and modify it. I have plenty of confidence in my ability to code well. What I'm not so sure about is whether or not I'm understanding what I'm supposed to be doing. It's all context. On the other end, the framework code represents a problem domain I feel I have mastered. It's good words and I can understand them, but it tells me nothing really about what I'm doing. It's context-free. It's not cool-looking code because I know how to code well. It's cool-looking code because I'm done with analysis.
The last example represents a problem where I'm both the PO and the coder. In this case I'm both deciding the general problem to solve and bouncing back-and-forth between understanding the problem better and changing where I want to go. It's a story for another day, but none of this has anything to do with how to code in F#. I don't code poorly or wonderfully based on how much F# I know. It flows in the opposite direction: F# shows me how much of the problem I know. In this way, great F#/Pure FP coding help can actually be detrimental to being a good programmer. You're becoming excellent at covering up the one thing the code is supposed to tell you: just what the hell you're supposed to be doing here.
And there's the problem and the reason for this essay: the more I clean up, the more I'm moving away from being able to maintain. Similarly, if I over-clean too early I end up with bugs I don't understand. I'm optimizing along the wrong path.
On the flip side, if my code passes the tests and I'm not going to ever look at it again? All of that noise collapses down into magic piping.[3] The universe is pleased. F# (the way I use it) forces me on the journey from solving problems to writing better code. I end up doing that over and over again. That both makes me a better programmer and somebody who can solve problems faster.
We're teaching and showing off how to program cool at the expense of teaching people how to actually be a programmer. That's fucked up. You shouldn't do one without the other. The topics don't split up in that way.
Code stupid. [4] Clean smart.[5] Always write code you can walk away from.[6]
Out.
- I come from a C++/C#/Java OOP background. As I got into pure functional programming, I struggled for a long time about the role of types and Domain Driven Design. I eventually settled on this conclusion [insert long discussion here]: Outside a pure function, only use types that have direct business value to that function, ie, microservices should implement true business features and only true business features. I began covering this topic in my essay "Incremental Strong Typing"
- Following this line of reasoning, I realized that strong types could (and should) exist outside of the microservices that implement them. It's called acceptance tests. I covered this in my essay "Writing Honest Microservices" Writing code this way, your microservices become pure functions, the OS handles resources and continuation issues, data is passed as untyped text, and strong types that live outside the code make sure that the entire thing holds together (instead of turning into 3,000-microservice mess) It's a compiler, it's just a compiler that works across the entire OS/Net stack. I covered this in my essay introducing supercompilers. See the DarkLang project and few other folks who are making a go of this.
- Even with this understanding, we coders tend to make things more complex and less understandable than we need to. Everybody says "keep unneeded complexity out of your solutions", but what does that really mean? I introduce a static code analysis technique that directly measures complexity using CCL in this essay.
- Coding is really wild because in many cases it can be as gnarly and complex as we want. When we ask if coding is hard, it's meaningless without all these other ideas (mentioned above) for context. I cover this in "Is Programming Hard?"
- I talk about "cleaning up" the code as you understand the problem more, but how does this work in teams? It sounds too woolly, too belly-button gazing. Well hell, if you're impervious to any of the rest of this advice, just use Code Budgets. That concept alone will proundly change how you write and deliver solutions.
- The most unanticipated aspect of re-evaluating how we code was realizing that there was no criteria for what made good code. No wonder there's so much coding crap in the world. What's the goal of programming, anyway? Aside from making the PO happy, what are we really trying to do? I argue that if you're not writing code you can walk away from, by definition you're part of the problem, not part of the solution, in my essay "Good Enough Programming"
Comments ()