Grace Version Control

0 views
Skip to first unread message

Shantelle Wenske

unread,
Aug 4, 2024, 11:38:46 PM8/4/24
to staricdisting
Imean, I love F#. It's my favorite programming language, ever. It's been the vehicle for me to learn how to think functionally about programming, after two decades of object-oriented[ish] programming. And I've never seen a language like this where almost everyone who starts using it regularly, loves it, too.

I know I could do some Using F# with ! posts. And I probably should. I've written around 24,000 lines of F# code for my version control system, Grace, for almost three years now, and it uses things like Dapr, Dapr Actors, CosmosDB, Azure Storage, Spectre.Console, and more. I've got some interesting, functional code in there that I could talk about. (I think.) But none of that felt like the right thing to write about.


Anyway, one of the things I've learned about getting unstuck creatively is that sometimes it's best to start just by espressing something about what's present for you, what's going on in your life, right now.2


So, that's what I'll share with you today: some not-great code I wrote a while back, why it's not great, how I'm fixing it, and what that might say about my development with F# and functional programming.


Grace is a new, modern, cloud-native version control system. It is multi-tenant, centralized, and relies on PaaS products like Azure Blob Storage and AWS S3 for storing versions of files. Because it's centralized, Grace needs to ensure that when commands like grace switch or grace rebase are run, that the existing state of the branch is saved and uploaded before changes are made to the working directory.3


Like you'd expect in a version control system, Grace has a command - grace switch - that lets you switch to another branch in your repo, and will update your working directory with the current contents of that other branch.


Grace also has a background agent mode - grace watch - that maintains a live connection to the server, enabling 2-way communication and local and server-side event processing, and that watches your working directory for changes and automatically uploads after every save-on-disk, allowing most of the more common Grace commands to run incredibly fast.


There's a lot that I hadn't yet learned about thinking functionally. When I look at it now, some of the code is clearly, "yeah, had no clue how to do that." Some of it is, "Awww... that's cute. At least I tried." Some of it is, "I need to come up with better names for those functions."


In the "had no clue" department, you'll find some of my code for Grace's command-line interface (CLI), specifically, the way I'm trying to interleave work being done, like calls to the server, or writing new local files, with UI updates (updating progress bars) that reflect the progress of completing the command.


As I was writing it, I knew it wasn't great. It all looked like the Pyramid of Doom [towards the middle of the article] that our friend Scott Wlaschin warned us about, that I knew I should avoid, but couldn't figure out how to, and didn't have the time to deeply work through. It worked, but it wasn't anything I was proud of. I haven't deeply dived into this part of Grace in a long time, and I knew it would be a challenge to get back into it.


I decided some weeks ago to start by rewriting grace switch to be more idiomatically functional, to clean it up as the first example of how to do it for the rest of the CLI commands. This was kind-of stupid, because it's the longest bit of code in all of the CLI commands, it's complex, and would be the most difficult to do. But it was deliberate: if I could make it work for grace switch I could make it work for the rest.


Let's start with the main part of the new grace switch handler function. In the local function generateResult, look! monadic bind! With that defined, we check if we should show output, and either create those progress bars, or not:


And instead of all of it in one huge function, here's a sample of some of that logic broken down into a smaller function that's easy to understand, with fewer places for bugs to hide, that handles its own output. This is the part that checks for changes in your working directory:


But... not exactly. After I refactored the code and started testing it, I found some bugs, and those bugs go all the way back to when I wrote grace watch and grace switch in the first place. Some of those bugs have now been fixed. A few remain that I'm tracking down.


It's almost 200 lines long. That may or may not sound like a lot to you, but, to me, now used to F# and thinking in terms of small, single-purpose, composable functions, it's a lot, it's probably too much, and it's a code smell.


In my first job out of college, in 1991, as a mainframe COBOL programmer for AT&T, I remember the senior programmer on the team telling me, "Most people can only hold about a screenful of code in mind at any given time," and that was back when we used 24 x 80 character green-screen monitors!


That same senior programmer had worked on that code for its entire six-year existence, and had previously worked on the system it replaced for many years. When I asked him questions about the code, like why a certain module was written the way it was, he would look up, scratch his chin, and start into a story like "well, about four or five years ago, there was this requirement..." or "in the old system, about ten years ago, ... and we never cleaned that up." It was always a story, and almost always something that, if you didn't have the context from that story, would be much more difficult to understand.


200-line functions are not good for comprehensibility or maintainability. They invite subtle bugs. They're hard to debug when you've never seen them before. (And, apparently, even when I wrote it myself.) If you get asked about why they are they way they are, you probably have to tell a story, and that's not good.


That's exactly what I'm working on: refactoring getNewGraceStatusAndDirectoryVersions and some of its related functions that are called both by grace watch and grace switch. It's a slog. I was trying to think in functions back then, but they're not shaped well, they do too much, and they're just hard to understand. I don't want to tell that story. I don't want to leave that for the next maintainer, who could be Future Me.


Well, I've gotten a much better sense of "how big is too big?" and "how complex is too complex?" when creating and refactoring functions. I might write a first-draft of a new function that's a bit long, but I immediately start breaking it down into smaller parts that are easier to understand, so I don't have to tell any stories for someone else to maintain the code.


I'm comfortable thinking about functions as first-class language constructs that can be passed as parameters, and I've gone through all of the phases of "I still use classes because I'm still embedded in OO thinking" to "I never use classes because I'm cool like that" to "I use classes where it makes sense".


Some of Grace is really clever and well-written, if I do say so myself. For instance, the way I do parameter validations in Server API endpoints is kind-of neat. I create an array of function calls that check the values passed in, return a custom error if they fail, and I execute them before I run the "real" action of the endpoint. It took a bit to figure out the right pattern, but once I did, repeating if for all of the API endpoints has been easy. Here's an example, from the endpoint to set the name of an organization:


validations here is a function that takes in a parameter object, and the HttpContext, and returns ValueTask array. With that in place, when I run the validations, I can use a utility function allPass to say:


I've never used statically-resolved type parameters (SRTP). I've never used or created a type provider. I've never created a custom computation expression (CE), and I still don't have an intuitive grasp of when creating and using one would improve my code. I have a basic understanding of category theory, but I'd love to deepen that and use more of the patterns from category theory where they make sense in my code. I've never written Haskell, and knowing Haskell, and being able to compare it to F#, would, no doubt, be helpful.


I say all of that to say: I'm not at the beginning of my journey in F# anymore. And I'm far from the end of it. I'm solidly in the middle, and that means that I'm good enough to be creative and concise, and to feel good about the code I'm writing now.


And you should be proud of where you are in your journey. If you're a programmer (and if you've read this far, you probably are) then you've had your experience of starting knowing nothing, writing and shipping code even though you weren't very good at it yet, and consistently improving, maybe even approaching mastery with the languages and tools you use.


I'm proud that I can see the progress I've made, and it gives me proof that I can still learn completely new things, move from beginner to experienced to mastery, and continue to develop both my art and craft.


Duo Free plan customers have limited access to Duo policies. Free plans may only control the New User Policy via a global or shared application policy. All other available application settings are configured at the individual application.


Only admins with the Owner or Administrator roles can create or edit policies. Admins with the Application Manager role may assign existing policies to applications, but may not edit or create policies.


While in the compact view, which is the default view, you can alphabetically sort your policies by clicking the "Name" column heading. The compact view shows the first few applications and groups assigned each policy, as well as the last-modified time and date if edited since October 2023.


Click Rules in any row to view details about that policy, like creation and modification timestamps (since October 2023), configured and unconfigured policy rules groupings, and the full list of group or application assignments. Click a policy's name to open the policy editor.

3a8082e126
Reply all
Reply to author
Forward
0 new messages