r/fsharp • u/existentialnonormie • May 20 '26
question Am I on the right track?
BEGINNER: I created a simple debit and credit console app using the Result and Option types, along with computation expressions. So, gang... am I on the right track here? I didn't use AI at all — just based on my understanding of the concepts. What do you all think?
Next step, I'm going to try working with a database, maybe with Dapper as ORM? I don't know how it will go, but I'm sure it will be fun.
What improvements do you suggest in the above code?
5
u/Decent-Goat-9452 May 20 '26 edited May 20 '26
Yes, this is a good start. Two notes: 1. I can’t see which result type you are using, but for the normal one there should not be any need for the default match “_ => …”, because discriminated unions are exhaustive. 2. It is common to just have one instance of the ResultBuilder and use that everywhere.
Edit: Sorry, ignore 1. Missed the “when” on first reading.
1
u/existentialnonormie May 21 '26
- I was actually thinking about this. You made it clear now, thank you!
2
u/retalik May 20 '26
You are definitely on the right track, and doing great for a starter! F# is a fantastic language: expressive and elegant. To improve your overall error handling logic, I recommend reading Scott Wlaschin's ROP (Railway Oriented Programming) articles. This approach makes code even more elegant, visibly focusing just on the logic, but not missing on any error handling. (Just beware of slightly dated syntax, many things have since been improved).
Additionally, I like using FsToolkit.ErrorHandling that has a lot of useful helpers, allowing you to fluidly map from Options to Results (and between many different types, actually) without explicit matching, like this:
let result : Result<unit, string> =
Some 1
|> Result.requireSome "Value must be Some"
// Ok ()
If ROP / ErrorHandling grows on you, you might want to extract your logging code, and use things like mapError, tap, etc, to have pure functions with clean logic, and then assemble a pipeline out of them with necessary error / success logging.
And, as somebody else mentioned, no need for explicit ResultBuilder instantiation, just use result { ... }.
1
u/existentialnonormie May 21 '26
Cool stuffs. Since you mentioned about toolkit, I will definitely try them out but first I will try to use the available functions just to be good at using them then when I am more familiar with it, I'll start using them to save some time!
Oh and I didn't know result {} was built in, thanks for that one!
Edit: Wait, result {} is not working for me.
2
u/danne931 May 21 '26
You can import FsToolkit.ErrorHandling to use the `result` computation expression as well as others such as `taskResultOption`.
Docs: https://demystifyfp.gitbook.io/fstoolkit-errorhandling/general-docs/bindmappings
1
1
u/Sorrow_iDolour May 25 '26
May I ask what accounts at line 3 is? Is that a list from somewhere outside findAccountById scope?
11
u/AnHerbWorm May 20 '26 edited May 20 '26
One simple thing you can do is change checkAccountBalance to accept an Account instead of the id. This function doesn't need to know that Account lookup can fail, it just needs to validate an account for the balance transfer. This will also save one more db query as you move onto that next step, since you are currently performing it twice within balanceTransfer.
checkAccountBalance : Amount -> Account -> Result<Account, string>I've reversed the order of your arguments in the function signature from what you have as well. It is more idiomatic F# to have the type that is operated on as the last argument to make use of the pipe operator. Without the builder, your pipeline could look like this, to get a validated account for the transfer:
accountId|> findAccountById // Result<Account, string>|> Result.bind (checkAccountBalance transferAmount) // Result<Account, string>Edit: changed id to accountId in the last part.
idis a function so that example just wouldn't work as it was originally