Now that we're programmers, let's make something
Lesson 1: How To Become a Programmer (In a Few Not-So-Easy Steps)
Lesson 3: Since We've Made Something, Let's Make It Cooler
So two days ago I posted a very rudimentary introduction into "how to become a programmer", which was mostly a rant about what is and isn't important, and about how F# works. (Why F#? Because it lets us be lazy, and it does a whole hell-of-a-lot for you.)
Today we're going to introduce the business domain.
Almost everything is "business logic"
The first thing about the "business domain" is that it's a very real thing. In the business domain everything is what's called "business logic", that is: logic that makes the business work. Now we're not talking "business" as in "for-profit corporate entity", but "business" as in "a person's concern". (Think: "it's none of my business".)
Basically, business logic is any logic that is related to the task at hand. So consider a "cart" system, business logic might be "we need to be able to add an item to the cart, remove an item from the cart, and change quantities of items in the cart." Simple, right? Somewhat straightforward. You may also here this referred to as "BL" or "domain logic", or the collective "domain" (which includes the "business domain" and the "business logic"). I'm going to call it "business logic" because that's what is most applicable to my situation, but feel free to use whatever terms you feel appropriate.
So we saw an example of business logic, what do we do with business logic? Usually we write our software around the business logic, so we may write a function addItemToCart
or removeItemFromCart
that adjusts the items in our cart as appropriate. These are considered "functions that perform business logic".
Simple, right? This is all largely basic, but it's important to know because we need to understand what is and isn't business logic. For example, when we disucss the implementation itself, we're no longer talking about business logic, but "application logic". The business logic is the broader picture: what is the purpose of our application? It's the "problem" the application is "solving", this can vary all over the place, but the basic idea is it's the higher-overview of the application.
All that long-worded rambling aside, let's define a piece of business logic that we want to build an application to solve:
Given a string, split it into a group of "words" on spaces, where each "word" has no-less-than three characters.
This is a very real problem I had to solve for my job recently, I won't say why, but it was necessary. So, we're going to focus specifically on this problem in this post.
The first step is to define the sub-steps
Every problem is, at it's core, a series of smaller problems. You can always break it down, though sometimes it's unecessary. For our problem let's break it down into subparts:
- Split a string on spaces
- Analyze the first word, if it has 3 or fewer characters, group it with the next word
- Repeat for the next word
So that's more manageable, and we can solve that in "sub-steps". We can solve each step independently. (Granted, we only have 3 steps, but we'll break step 2 down futher when we get to it.) Each step should be testable: so given some input I should have a defined output.
Splitting a string on spaces
So the first step is actually the easiest, split the string on spaces. in F# this is easily done via String.Split
, which is available in the entire .NET framework:
let parts = "Some String With A Short Word".Split(' ')
So that's easy, and we can create a function that handles that pretty easily, we'll define a split
function that takes a string, and a char (separator) and splits the string on the "char" (separator):
let split (c:char) (s:string) =
s.Split(c)
That's pretty basic, but why did we define a new function when we could just call s.Split(c)
directly, with our own s
and c
? This is so that we can split strings "idiomatically", that is, matching the general style of F# code. F# isn't about calling methods, it's about composing functions, and you cannot compose String.Split(char)
easily like that, so we define a function that lets us do so.
So now we could test our function, which involves simply calling it:
let parts = "Some String With A Short Word" |> split ' '
Well that was easy. This shows that we can pipe a string into the split
function, and it does what we expect.
Moving to Step 2: this is going to hurt
So F# makes step 2 pretty easy, and if you have any programming experience with a non-functional language, I want you to forget it right now. What you think you know is not true in F#, and we need to redefine the problem.
We can break step 2 down a little further:
- Get a word
- Check how many characters the word has
- If < 3, it belongs with the next word
So let's start building a groupWords
function, we're going to do this the "Elliott Brown" style, which is probably different from what you've usually seen, but this is where functional languages make things pretty awesome.
Instead of looking at the current word, we're going to look at the previous word, since it makes things easier. We're going to use a basic pattern match with a guard clause, List.fold
, Array.toList
, and Array.ofList
.
The easiest way to do this involves converting the string array
to a string list
, which is done with the Array.toList
:
let stringList = parts |> Array.toList
If you're following along in a REPL (the F# interactive window), you should notice the biggest difference between the printed versions of each is that Array
has vertical-pipes on the inside of the brackets: [|element1; element2; ...|]
, and List
does not: [element1; element2; ...]
.
So now we're going to fold over that list: a fold
takes an accumulator, and a value. It iterates over each value in the List
and applies a function to it, which usually combines the value with the accumulator somehow. Our fold is pretty basic:
let groupStrings (acc:string list) str =
match acc with
| prev::tail when prev.Length <= 3 -> (sprintf "%s %s" prev str)::tail
| _ -> str::acc
let groupedStrings = stringList |> List.fold groupStrings []
We could modify this to take a length instead of 3, but I'll leave that up to you.
Some neat syntax things:
- The
match
is a "pattern match", similar to a C-style language switch
, but on sterroids.
- The
prev::tail
is a pattern expression for a list: it means "match the first element in the list to prev
, and the remaining elements to tail
.
- The
when ...
is a "guard clause": it means this expression is matched first, then the entire pattern is matched if and only if the guard clause returns true.
- The
->
means "return the right side as the result of the match
expression." (Basically)
- The
(sprintf "%s %s" prev str)
just combines the two strings with a space between them.
- The
::tail
now creates a new list with the sprintf
block as the first element, and the remaining elements as the, well, remaining elements.
- The
_
is a "match anything" pattern-expression.
- The
[]
is an empty list (the type is inferred to be whatever that "folder" expects - a string list
in this case).
Now we just need to reverse the list, which in F# is easily achieved by List.rev
:
let finalResults = groupedStrings |> List.rev
And lastly, we'll convert them back to an array with Array.ofList
:
let result = Array.ofList
That's it, make it reusable
So now we've successfully built the functions to do this work, let's build a function that is reusable.
let splitAndGroupString =
split ' '
>> Array.toList
>> List.fold (fun (acc:string list) str ->
match acc with
| prev::tail when prev.Length <= 3 -> (sprintf "%s %s" prev str)::tail
| _ -> str::acc) []
>> List.rev
>> Array.ofList
And we can call it as:
let splitStr = "Some String With A Short Word" |> splitAndGroupString
And that's it!
We went through some basic business logic today, we'll be going through more complex stuff in the coming lessons, but it's good to take it slow (at first), and we'll eventually build up to some pretty complex operations.
I told you I would get more organized, and I did. As we continue I'll figure out how I plan to present things effectively, but for now I'm just going to stick with my gut. I hope you enjoyed it and learned something, but if not it's still helping me learn more and become a better software engineer, so you should probably count on more crap coming from me here soon.