The general value of typed functional programming lies in leaving no edge cases
np.reddit.com
np.reddit.com
head :: [a] -> a
-- versus
saferHead :: [a] -> Maybe a
-- versus
safeHead :: NonEmpty a -> a
The first function is partial because we'll get an exception if we give it the empty list.The second function is total but annoying to use because we're telling all the code that uses the result to check for two cases (it's an error not to).
The last one forces the responsibility on the caller to provide a non-empty input.
That was probably one of the more frustrating aspects about learning to program Haskell as someone who has been programming for more than fifteen years when I started. It revealed to me in stunning detail all of the edge cases that almost every other language I've used actively hides from me.
It doesn't absolve you of having to think about edge cases: even Haskell throws run-time exceptions. However it does give you tools to think about many of those edge cases up-front and in a direct way.
Update Added a trivial example to demonstrate "partial," etc.
headOr :: a -> [a] -> a
which is similar to the Maybe solution but forces the caller to discharge the Maybe immediately rather than letting them potentially clutter up the rest of their code by proliferating the Maybe.Although if someone doesn't know haskell, then the above notation for functions getting the first element of a list is probably not of much value to them.
Clearer would be:
char head(List<char> x);
char? saferHead(List<char> x);
char safeHead(NonEmptyList<char> x);It's why I think it competes for market share with some of what is currently Java/C# and similar languages. A lot of people who use those languages care about correctness, and Rust is big step up in this regard.
In some cases, that's not a problem or it's even the best thing to do. However, you end up writing this way all the time in an exception-based language, which is where the problem arises.
Non-exception based languages tend to throw the problems in your face and make them something you can't ignore. Which is not fun. But it is often what you need.
For example, say you have a pandas dataframe with a column "foo" and you accidentally try to read column "fooo". Instead of getting a useful exception like "PandasException: Attempted to read nonexistent column 'fooo'", you get a traceback 10 layers deep, terminating in something like "pandas.hashtable.PyObjectHashTable.get_item" which ought to be totally invisible to you, the library user.
Edit: by "this" I meant to agree with parent: Getting a useful, well defined exception is how it should be done. Psycopg2 does this very well.
Overall Python has really good stack traces, but the hyper-dynamic nature of so many common libraries does make it tough to grok sometimes.
I usually end up reaching for the PyCharm debugger which is fantastic anyway.
Maybes explicitly express what the programmer has foreseen, exceptions form a safety net over it. You need both in a wholesome language. That's wht even Haskell has exceptions.
That's a lot of possible error to handle! I don't like exception's invisible flow but I think that they are good for 'should never happen' errors.
I'm not sure that's such a bad thing. Exception handling doesn't mean you should be handling all exceptions, just that you can recover if one does occur. If an exception is 100% relevant to what you're doing then yes absolutely handle it, or even clean up your own state before propagating the exception if you can't. But please don't catch any and all exception to suppress or obfuscate them by wrapping it in another domain specific exception.
You can't because you don't know where it came from so you don't know what happened, why, or if it was recoverable. You can take a stab at it, but that's it.
Maybe that's a particular language constraint? In general exceptions have a stack trace and it's evident where an exception came from.
At least in my experience exceptions don't have to be handled unless or until they're disruptive. If you have a scheduler or task processor code that needs to run continuously then you're going to have robust exception handling, but not necessarily comprehensive or appropriate. For other areas of code however the exception handling isn't necessarily requisite.
Result/Maybe types are awesome, because you just don't get unexpected runtime exceptions due to programming error. You only get them when a genuine problem occurs.
That's a valid concern. I've seen attempts to document the custom exception types that classes can throw but it's never discoverable. It's compounded by the fact that uncaught exceptions bubble up.
I don't know that most people would sift through all the possible exceptions and write selective catch blocks to handle them. I think in general they will either catch all exceptions or none, if or until they need to handle a specific scenario that presents itself in production.
My perspective though is primarily of LOB Apps or Integrations and not widely consumed public code or apps.
But from my understanding, this is not directly related to the borrow checker, which tracks who owns a reference and is allowed to modify it or not.
(That being said if you had a "default" case earlier, the compiler won't complain)
(I don't remember the specific underlying feature flag name, sorry).
EDIT: It's called `-Wswitch`
> Warn whenever a switch statement has an index of enumerated type and lacks a case for one or more of the named codes of that enumeration. (The presence of a default label prevents this warning.) case labels outside the enumeration range also provoke warnings when this option is used (even if there is a default label). This warning is enabled by -Wall.
The real problems with C enums aren't about matching completion, they are that:
- There is no guarantee that a variable value is in the enum range;
- A C (or Java, or C#) enum is a very poor type and can not represent all the data that a Rust enum carries. Developers usually use them with union types to solve that problem, but then there aren't any warnings for most problems anymore.
In Rust, enums are a core data structure with completely equal status to structs, and are used pervasively throughout the language including the standard library.
Notably, both Option (which is used instead of null) and Result (which is used for error handling) are enums, which means that you thse correctness checks for every null and every error condition, on by default. Thats a huge deal as these are often the source of unexpected runtime errors.
C++ had boost.variant which did exhaustiveness checking since ... 2002 (and an std version since C++17).
This is just basic type safety stuff.
Edit: typo
By the way, now that I know some haskell, I see this edge case sloppiness all the time and it is really annoying. For example, here is something that annoyed me just recently. You go to yahoo finance and there they will show you the revenues of a company as well as the revenue growth from past year. But sometimes this revenue growth number is not available. For example, the company may not have existed past year, or perhaps it was not public past year and thus it did not publicly disclose its revenues. When they cannot compute revenue growth for these reasons, yahoo finance will helpfully show revenue growth as "N/A", i.e., not available.
That seems all nice and logical, but I noticed that yahoo finance tended to show that revenue growth was not available for many established public companies for which they should have the data. I looked at it more closely and noticed with shock that if the revenue growth of a company was about 0%, yahoo finance would still show it as "N/A"! So some programmer just decided to use 0 as not available and just erased a lot of useful information from their system. Now when I see a company that has N/A as revenue growth on yahoo finance I have to go and check whether the revenue growth was zero, or it was truly unavailable. As I said -- really annoying.
This is of course a textbook case for Haskells Maybe concept. You can have a type that is a Maybe Int, and that means it have a value or it can be Nothing. Thus, you encompass the edge case of not having the value available, without using an embarrassing hack that deletes real data. But of course Haskell gives you more power than just using Maybe. You can create a type that can encompass all kinds of edge cases and information about them. For example, you can embed a reason why the revenue growth data was not available, if that is the case.
If you encompass it this way. Literally nothing stops you from encoding this behavior in Haskell. Because the behavior you described is *if revenue is equal to zero, display it as N/A".
No magic monad will prevent this.
If your data and/or problem is modeled as "if 0 then N/A" no amount of "human nature implicitly explicitly transformed" will help you.
Dealing with non-existent data as opposed to "value = 0" is just as natural in Java, C++ or Javascript as it is in Haskell. And yet, that didn't help.
This is not unique to functional programming languages.
Haskell was definitely a trailblazer for this, but plenty of conventional languages support this now with relative ease.
I don’t know of any mainstream language that has bolted on that level of verification.
typedef struct{int filled; union{void* data;} val;} maybe;To do something like x.get() in Scala when x is empty is a runtime error, not a compile-time error.
But Kotlin? Since when does Kotlin have pattern matching with exhaustive checks? Also it does not have an `Maybe a` / `Option[A]` type out-of-the box.
sealed class Communication
data class Email(val emailAddress: String) : Communication()
data class Shouting(val preferredName: String) : Communication()
object HiveMind : Communication()
fun exhaustiveWhen(comm: Communication) {
// not exhaustively checked (will compile)
when (comm) {
is Email -> println("sending email to ${comm.emailAddress}")
}
// *is* exhaustively checked (error at compile time)
// must add HiveMind or 'else' branch
val result = when (comm) {
is Email -> println("sending email to ${comm.emailAddress}")
is Shouting -> println("HELLO, ${comm.preferredName}!")
}
// *is* exhaustively checked
when (comm) {
is Email -> println("sending email to ${comm.emailAddress}")
is Shouting -> println("HELLO, ${comm.preferredName}!")
}.let { }
}Even the syntax can be made less awkward[1] this look still like a wonky hack to me where the user can do way to much wrong.
[1] https://proandroiddev.com/til-when-is-when-exhaustive-31d69f...
Whether a type is nullable or not is a property of the type. This creates more or less two mirror universes of types, and you can't mix even "the same" types from that incompatible universes.
An Option type is on the other hand side a distinguished parametric type on its own (I wrote `Maybe a` / `Option[A]` for a reason). It can "wrap" arbitrary other types without "infecting" those with the nullability property. Being a proper parametric type makes `Maybe` / `Option` also "just a type constructor". Therefore it can be composed with other types and type constructors according to common rules. `Maybe` / `Option` doesn't require any special rules in the language (which would make the language more difficult to learn and use)!
Also `null` is very different form `Empty` / `None`: A null value can still "sneak in" through arbitrary references and cause run-time errors if one has to interoperate with a "null unsafe" language or system (and that's more or less "the whole universe" out there). A `None` can not "sneak in" as it can be only assigned to something of `Option` type. Sure this doesn't solve the "null problem" as such but one can at least distinguish the cases where the model value is really optional, and the cases where one needs to deal with `null`s coming (potentially) form the outside world. In the one case one will use the Option type throughout the rest of the program, in the other case one will preform some null-check as early as possible and "unwrap" the underlying none-null value to work with it directly thereafter.
/s slightly, maybe more snarky, but seriously, optional types are way overhyped. Null objects are far more useful than optionals. Optionals tend to leak all over the show.
If you wish to reduce "leakiness", you can dispatch on the null-ness of the variable; Swift lets you write `if let x = x { /* x is non-optional in here * / }`, which binds a non-optional value inside the braces, and `guard let x = x else { return } /* x is non-optional now * /` which binds a non-optional value after the braces. So as soon as you consume an optional value, you are free to "un-leak" it and extract its value (if it has one) immediately.
For example consider a network response that in a JS app might be passed around as an opaque object. In a language like Swift this is infeasible, you have to transition from the network serialized version to concrete models at the network layer. If you encounter mismatches in the expectation of what is optional you have to handle it here at the edge of the program. This enables massively safer code and makes reasoning about your program much simpler if anything.
As a concerete example say an API response tells you if a user is verified(for some domain specific definition of "verified"), additionally you want to know when they were verified maybe to give them a hint when they need to do it again. You coudl model this as:
{
"verified": false,
"verified_at": null
}
before they have verified and {
"verified": true,
"verified_at": "2020-02-24T15:31:12Z"
}
after they have verified.In Swift this would then be modelled as
struct Users {
let verified: Bool
let verifiedAt: Date?
}
However this introduces an Optional which complicates code that needs to deal with users who are verified or not. Instead you could model the same thing and avoid this issue as: {
"verification_status": {
"status": "unverified"
}
}
for the unverified case and {
"verification_status": {
"status": "verified",
"at": "2020-02-24T15:31:12Z"
}
}
for the verified case. In Swift this can be nicely modelled without any Optionals using an enum enum VerificationStatus {
case unverified
case verified(Date)
}
struct User {
let verificationStatus: VerificationStatus
}
Better optional value visibility leads to better data modelling.:)
From C! AFAIK C++'s references can not be null!
Better be sure to call the "isNaN()" and "isInfinite()" methods all over the place, because those are the worst edge cases of all and your language provides no help in detecting or avoiding them. It's like having 3 or 4 other None values, where they don't even use the same system as your standard Option type.
If I had a nickel for every time I saw "NaN" appear on a webpage or a web browser console log ... We throw up our hands and say, yeah, well, JS is terrible -- but no static functional language I know of is any better.
You might be thinking "what about libraries that require floating-point", and that's a good concern. Luckily in Haskell there's the Num typeclass, so your libraries can let users use whatever fractional number type they'd like.
# where R is some r such that Num r
let huge = <the largest (finite) value in R>
let inf = huge * huge # can't be finite unless you
# truncate or saturate or something
let nan = inf - inf # can't be either of
# infinite (infinitely < +inf and > -inf), or
# finite (catastrophic rounding errors)
Num doesn't allow inf or nan to be Maybe R, so you're stuck filtering out in-band errors from a bare R. quicksort :: Ord a => [a] -> [a]
.. quicksort [] = []
.. quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
.. where
.. lesser = filter (< p) xs
.. greater = filter (>= p) xs
quicksort([sqrt(i)|i<-[0 .. 3]])
=> [0.0,1.0,1.4142135623730951,1.7320508075688772]
quicksort([sqrt(i)|i<-[-3 .. 3]])
=> [NaN]
quicksort([sqrt(i)|i<-[3 .. -3]])
=> []
Now, just for fun, let's reverse the condition... quicksort :: Ord a => [a] -> [a]
.. quicksort [] = []
.. quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
.. where
.. lesser = filter (\x -> not(x < p)) xs
.. greater = filter (\x -> not(x >= p)) xs
quicksort([sqrt(i)|i<-[0 .. 3]])
=> [1.7320508075688772,1.4142135623730951,1.0,0.0]
quicksort([sqrt(i)|i<-[-3 .. 3]])
=> [1.7320508075688772,1.4142135623730951,1.0,0.0,NaN,1.7320508075688772,1.4142135623730951,1.0,0.0,NaN,1.7320508075688772,1.4142135623730951,1.0,0.0,NaN,1.7320508075688772,1.4142135623730951,1.0,0.0,NaN,1.7320508075688772,1.4142135623730951,1.0,0.0,NaN,1.7320508075688772,1.4142135623730951,1.0,0.0,NaN,1.7320508075688772,1.4142135623730951,1.0,0.0,NaN,1.7320508075688772,1.4142135623730951,1.0,0.0]
The last example will explode with larger values, like [-100..100].Note, it is actually the first time I tried Haskell. And no, I don't think the language is broken because of that. In fact, it is exactly the behavior I expected.
But you have to understand that the whole situation is a bit complicated. The thing though is that Haskell gives you possible ways to handle those complications.
It's actually not exactly a decision being made by the language designers. The IEEE standard governs the floating point logic embedded in ALU hardware. Programming languages tend to expose floating point math in its rawest form for performance reasons.
I was, however, surprised to not see any kind of a (significant) `safeDiv` function on hackage, and no NonZero newtype (outside of quickcheck). But, for this kind of thing its so easy to roll yourself, I am guessing this is what people do (and I have seen).
Hey, it's for sure nice to have a rich numeric stack (see also some lisps), but in numeric work "use something else" is very rarely a practical answer to issues with IEEE-754.
There seem to be a large disconnect here. A big chunk of people don't need the speed that FP math brings, what they need is exactness. And, for most languages with a C-based lineage, actually correct floating point math support seems to fit in the "someday/maybe" categories.
https://www.youtube.com/watch?v=8_HsFrXhZlA
> And now you can do the most important thing you can do with any piece of code... stop thinking about it! Go home! Pet the cat! Watch House with the wife! ... I love Friday deployments ... you know why? Because I know what my code is going to do before it runs!
I deliberately didn't go into the specifics about the various typeclasses at play and their laws, and how those are tested. Part of that is just for reasons of time, but the more salient reason is that I wanted to show how those of us who do purely functional programming do it in practice. So if you watched the presentation, you saw that most of my thinking was about "OK, how do I elaborate from the most trivial transformation (do nothing at all) to the one I really want?" And that proceeds compositionally: I transform this value to that value. OK, does that value have the right type? No? What do I need to do to ensure that it does? And the transformation steps have some important properties, like their scope being entirely local. I really tried to emphasize this at the end with `attemptRepeatedly`: my description of each line of the whopping three lines is exhaustive. When I say there's no point in writing a test for it, I mean that literally. There certainly are typeclasses at play, and I briefly talk about the `Catchable` typeclass, and show the ScalaDoc for it, which documents an important law: the relationship between catching ambient exceptions and the algebra provided by `Catchable`. I rely on the other typeclasses and other laws in a similar fashion.
The other thing I think is pretty important is the part where I say "Let's look at cases," because the point there is that I can reason about the code by reasoning about the shape of the data it's manipulating. So if a `step` is an `attempt` of `p` that, if successful, `kill`s the retry `schedule` or, if unsuccessful, logs the exception, I'm still dealing with a `Process` of one element (by assumption that `p` will emit one element). Then `retries` will be a `Process` of 0 to infinity elements, because we put no constraints on `schedule`. So `(step ++ retries)` will be one or more elements, and because `step` `kill`s `schedule` on success, because `retries` is derived from `schedule`, `retries` is also `kill`ed. So `(step ++ retries)` is a `Process` that will emit one or more elements, with the last element being the first successful one, or the last failed element if none of the `retries` succeeds, so we take `last`. Then we just `fold` the failure or value back into a single effect, and we're done.
As I discuss in the presentation, there certainly are questions. What happens if the `schedule` is empty? What happens if the `schedule` is infinite? An attendee in Q & A asked a really good question: was I sure `retries` would wait before the first retry, or was the semantics "try, then wait?" (It really is the former, but that wasn't clear from my recorded REPL session.)
Of course, this isn't very impressive for a three-line example, although I think it's pretty striking that it only takes three lines, each of which can be completely reasoned about independently, to achieve a pretty significant operational goal. The point, though, is we can build entire systems this way, and in fact `attemptRepeatedly` is part of a distributed monitoring system I worked on at Intel Media/Verizon Labs, which is written entirely in this way apart from the monitoring types themselves, which present an imperative API for familiarity's sake (and which we later came to regret).
I hope this helps!
Functional languages have many of these patterns already encoded into common functions, like map and fold. As functions their correctness can be considered mathematically proven. Would you rather rely on convention or the compiler?
Consider adding on top of functional safety/correctness type safety with a functional language implementing a strong type system, like F#. Sure, F# is only functional first, not purely functional, and probably all type system have some sort of implementation edge issues. For most practical purposes you can consider the added level of type safety imposed by the compiler also to be mathematically provable.
It means you operate on a higher level of abstraction, and that inherently makes reading code faster. Not per line of code, but certainly per functional unit.
Idiomatic Rust tends to use a lot of functional code, and provides all the tools to make that safe. Its map function could mutate the input -- it would be unidiomatic, but certainly possible -- except that, if you don't pass a mutable reference, then you're safe to assume it won't.
for i=0 to 10 {print(a[i])} vs for x in a {print(x)}
With functional programming it gets even better:a.filter {}.map{print}
Pseudo code, of course. I list several “functional“ examples in Swift here:
https://github.com/melling/SwiftCookBook/blob/master/functio...
Functional programming is also a higher level language. map, reduce,filter, flatten, flapMap, drop, take, zip, ...
Learn the concepts in one language and they can be used in another functional language.
a do: [:x| Transcript print: x ]
[] = closure
That is, because Smaltalk was based on Lisp. But the other OO languages removed this concept, but it came back in Scala.Lambdas have some allocations. I like LINQ but it can have some allocation as well.
These are all tiny allocations but in a game you'd like to strive for 0 allocations.
1 Storing a primitive as a first class object
2 packaging and repackaging data
There may be situations where the former is not an issue, but the latter often is. If you don’t have a way to treat filter() as a generator/coroutine it means your traversing one collection and inserting those values into another, only to pass it to map() which iterates again and creates a third. If you’re doing that once a frame, maybe not a big deal. Three times per object per frame? That’s a lot of temporary data structures.
If the other responder is right, I speculate that the borrow checker forced someone’s hand in Rust and they found that a generator keeps the borrowed objects on the call stack where they already had tools to track ownership.
for (auto x : someContainer) { //do something with x }
But inevitably, I end up needing either an iterator pointing to a particular item at the end, or I need an index to pass to some other function, and this style makes that impossible. Perhaps it's just the domain I work in, but even after years of trying to make this work, it still only works about half the time for what I'm doing.while (my ($index, $value) = each @list) { ... }
https://docs.raku.org/routine/kv#class_List
for @list.kv -> $index, $value { ... }
map someFunction listOfValue
if you need an index: map someOtherFunction (zip [0..] listOfValues)
Python uses 'enumerate' to similar effect. Not sure if C++ has an equivalent? zipWith someOtherFunction [0..] listOfValues
# , since someOtherFunction is likely to be
# :: (Int -> Value -> Stuff) rather than
# :: ((Int,Value) -> Stuff) .
zipIndex f = zipWith f [0..]
# is also often useful.In eg OCaml you have to use something like zipIndex (but implemented differently) or mapWithIndex, because they don't do lazy data structures by default. (And in eg Python their lazy sequences come with lots of caveats because they are not referentially transparent.)
Just change your loop to auto& x.
This does not invalidate your experience. It's common, and with good reason. This sort of dissimilar experience with the same code is why coding standards need to be views as social norms for the team using them, and not just technical things.
The functions map and filter are nice and restricted. If you want more power, you use eg a fold. And if you need even more power, you write a new recursive function.
So a quick look at the code will tell you what to expect.
For me the argument holds in practice as well. I'm mostly reading my combinators in Haskell, and the loops in Python or some curly brace language.
If you use a general purpose combinator like mapAccumL that has almost as much power as a loop, you don't get that much extra readability compared to the loop.
It's still a bit better, because that combinator still has to process one element of your list for each run through it's "body"; and it clearly warns you by it's very name that it's not just a filter or map, so there's less possibility of confusing it for something simpler.
And, of course, not all loops are created equal. When applicable, for-each loops are much cleaner than classic C-style for-loops for example.
I moved from first camp to the second one somewhere during my adult life. And IMO: Both are valid, sometimes first one is necessary (because that's how the machines actually work), but second one is clearly better most of the time for humans, especially in business-logic heavy cases.
It's the same dichotomy; are you thinking of f(x) as an instruction to do f to x, or are you thinking of X_F as what happens when X goes through F?
Or you can use types where it's enforced by the compiler.
But it may be a very interesting question for a researcher.
Business logic relies so much on run-time values that (IMO) using data and predicates is a better fit.
try {
// 500 lines
catch (e) {
return 4;
}(More pithily: try/catch is not inherently non-functional.)
This seems not that different from throwing an exception, except the caller can’t accidentally forget to deal with the colinear case, there must be code to handle the Nothing case.
But perhaps the question you’re asking is “but how can the writer of toPlane() know THEY did the right thing”. There’s of course no solution to logic errors (function subtract(a,b) { return a + b } will get by most type systems, short of having the type system re-encode the function itself, at which point it’s just correctness through redundancy — unless both the type AND function are wrong!).
However, you COULD protect against future people breaking your implementation by doing something analogous time weak_ptr’s implementation and flipping the Maybe to the input vs the output). So for example, you could make a type NonColinearSet, and have the “constructor” take a PointSet, but return a Maybe<NonColinearSet>, so asNonColinearSet takes a PointSet and returns Nothing if the PointSet is colinear, or NonColinearSet if it isn’t. Now you computePlane() function can take a NonColinearSet and return Plane (not Maybe<Plane>) since it “knows” the input must be valid, so it can ignore edge cases internally, at the cost of the caller having to deal with toNonColinearSet returning Nothing before calling it, since you won’t be allowed to transparent pass the result of asNonColinearSet(pointSet) to computePlane since the types Wong match (Maybe<NonColinearSet> vs. NonColinearSet).
maybeNonColinear = asNonColinearSet(pointSet);
case Nothing: alertUserBadDataOrWhatever();
case Just<NonColinearSet>: return toPlane(maybeNonColinear.value)
Notice in both cases, ultimately the caller must do something in the edge case, which is what you want.EDIT: and, as neighbour says, any in typescript ! But typescript, with its progressive typing, is in a league of its own.
Then again, it's not always without hiccups.
I've seen this over and over in my career; tools can make people lazy in certain areas, it can put them on auto-pilot. Often, this can be a bad thing.
I like coding with dynamic languages because they get out of my way and shift the full responsibility for correctness on me. This creates a certain mental tension which helps me to perform. Keeps me alert and mindful.
I particularly try to avoid languages that make me wait for code to compile (which is more common with statically typed languages); this is for the same reason why don't like it when someone distracts me while I'm "in the zone" while coding.
But between these two exist a permutation of conditions that exists in the domain and often manifest in production. Either we can actively manage them thanks to types, or we can let it end up in production with hard-to-track bugs.
Sometimes safety equipment lets you go faster.
Even simpler: brakes let you go faster.
When I was cycling a while ago, my brakes broke in the middle of a cross-country ride. I didn't want to push the bike back all the way, but you can bet your hat that I rode extremely slowly and cautious.
I don't think anybody remembers how shit bike brakes were before dual pivot came along. God bless Shimano and keep it forever. When I got my first set, I started emergency-braking with 2 fingers because I was worried I'd pole vault myself if I actually squeezed like I meant it. (I have high grip strength).
I was only a few kilometres from home.
In the mean time I will just keep using my strongly typed language and, of course, also automated tests. Thank you very much.
And of course, lines of code that you didn't have to write are less likely to have bugs in them.