Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added FSharp samples #13

Merged
merged 20 commits into from Nov 17, 2022
Merged

Added FSharp samples #13

merged 20 commits into from Nov 17, 2022

Conversation

oskardudycz
Copy link
Owner

No description provided.

@oskardudycz oskardudycz marked this pull request as ready for review October 21, 2022 18:55
@isaacabraham
Copy link

You might want to consider using Fantomas as well to ensure consistent formatting.

@oskardudycz
Copy link
Owner Author

oskardudycz commented Oct 22, 2022

@isaacabraham

You might want to consider using Fantomas as well to ensure consistent formatting.

Thanks for the hint! 🙂 I added it explicitly, although I'm using Rider, but (unfortunately) Rider doesn't apply autoformatting on change, so I tend to forget about running that manually. Do you know if running Fantomas with a dotnet watch is possible, or what's the common way of handling it? I know that I could use a pre-commit hook, but that's a bit too late for me. Is it possible to get a similar experience as with eslint with JS/TS stack?

match bankAccount with
| NotInitialised _ -> invalidOp "Account is not opened!"
| Closed _ -> invalidOp "Account is closed!"
| Open openBankAccount ->

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider replacing openBankAccount with x - if you keep functions and expressions short, x can be "the thing we're working on". And if it's not obvious, thats a hint to extract to a function

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or even value.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, value or something that names the role is definitely an excellent default
Using short names like x is definitely a subjective thing with downsides.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the functional flavour, but I prefer to keep the longer version. I understand the reasoning; it may happen that in the future, I'll change my mind, but for now, I don't like the single-letter naming. Especially the one I saw in Haskell code was making it too esoteric per my taste 😅 I know this may not be following the F# best practices, but I prefer to keep explicitness here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no official F# best practice on this

There are people who try to write Haskell in F#, and they definitely would use x. I'm not suggesting it from that angle of zealotry - I just happen to find it makes code regular and easy to read if you have standard cues and patterns in layout and naming.

I'd equally use the name value in C# too on the basis that it's a role name - explained better in https://blog.ploeh.dk/2020/11/30/name-by-role/

type BankAccount =
| NotInitialised
| Open of
{| Id: AccountId

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably do this too, but stuff will be much nicer if you stop using an anyonymous record type here
(put them at the end of the DU type defintions with and OpenState = { id: AccountId; balance: decimal; version: int64 })

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know some resources around where to select which style? I tried to understand when to use them, but so far, no luck. I've stolen this syntax from your PR in my samples 😎

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the Stylish F# book covers these sorts of tradeoffs and is a good book in general (have only read a chapter or two so not a full assessment though all of Kit Eason's stuff anywhere is absolutely fantastic).

In general, as in writing, forward references (i.e. and is something to use sparingly). There's definitely a value to knowing that you can scan a file top to bottom to find things. If you pull it out in the above, I believe it will. help readablity in that one will be able to glance and see there are 3 cases, of which 2 have values. If you put the body record type defs immediately below, with or without a blank line in between, most readers can instantly get it whereas an 8 line block with inline anonymous record types is harder.

Another thing I tend to do is define records on a single line. The tradeoff between e.g. 3 vertical lines (which makes it harder to scan the code overall) vs having to pick out a field when 3 are on one line is obviously subjective, but I use it as a forcing function to make myself pull out sub-records if the line is getting long. Jumping straight to multi-line format just increases the temptation to whack everything into one record (with flags instead of modelling cases etc leaking in too when you go down that road!)

I believe there is an fslang-suggestions issue about relaxing the anomaly of not being able to construct anonymous records without special syntax. If that was implemented, there would be no negatives. However for a case body type that's used lots of times giving it a proper definition and name of its own is probably worth it.

Of course there are negatives to giving something a name that's pretty pointless - i.e. my suggestion doesnt make the code easier to read. Once the logic in the decider gets more interesting, you'll often find yourself wanting to utter the name of that body type anyway, which forces the issue.

So, in general, I tend to start with anonymous records but if I have more than 2 places instantiating them, I'll give them a name.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the long explanation. I think that I need to finally read Stylish F# book, plus I need to get better "gut feeling". Tho that can be earned by hours spent in coding 🙂

RecordedAt = command.Now
Version = openBankAccount.Version + 1L }

let closeBankAccount (command: CloseBankAccount) (bankAccount: BankAccount) =

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type annotations probably not required.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm still anxious about using type interference everywhere. From my past experience, I prefer to name explicit types where I know that I don't expect to have any other type ever. Thoughts?

Copy link

@bartelink bartelink Oct 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legibility should be the main driver - if you think mentioning the type will give a useful cue to a reader, thats a good reason to put it in. If you have 50 in a row the types, parens and colons make a mess.

Pinning the type can sometimes even show up in compile perf (only with pretty large assemblies though - this is not anything to worry about).

The other reason to pin even when not strictly necessary is if something is used in lots of contexts and is low in the stack - sometimes a bad inference can happen based on the first usage which then ripples as lots of compiler errors from all the other usages.

In this particular model, where you have lots of records in scope with lots of overlaps in field names, being explicit about types is probably a good thing. In general I'd say as you model real things more thoroughly, those reasons melt away and leaving the types off will be the right thing - the types will be beside the point and variable role names are the bit that make it all legible far more than the names of the types themselves.

match bankAccount with
| NotInitialised _ -> invalidOp "Account is not opened!"
| Closed _ -> invalidOp "Account is closed!"
| Open openBankAccount ->

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or even value.

Updated project files structure to not have redundant options and Program
@oskardudycz
Copy link
Owner Author

Folks, thank you once again for the massive feedback! I appreciate that a lot and have already learned so much from you. I wrote a short summary of what I've learned in my blog: https://event-driven.io/en/writing_and_testing_business_logic_in_fsharp/.

You rock! 💪🙂

@isaacabraham
Copy link

@oskardudycz In terms of Fantomas and Rider - it does have formatting supporting and even Fantomas integration but I don't recommend using it, because it mixes Fantomas with its own internal formatting settings. What I've used in Rider instead is a File watcher, configured on any FS file change, just to run dotnet Fantomas {Filename}. that last placeholder probably is wrong, I can't remember the exact definition in rider, but it works totally fine.

You can of course set up things in a CI pipeline as well - Fantomas has a mode where it'll fail if any files don't match it's syntax etc.

@oskardudycz
Copy link
Owner Author

@oskardudycz oskardudycz merged commit d359631 into main Nov 17, 2022
@oskardudycz oskardudycz deleted the fsharp branch November 17, 2022 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants