Reputation: 12087
let's consider this code:
let getBuildDate (assembly: Assembly) : DateTime option =
let buildVersionMetadataPrefix = "+build"
let attribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
if attribute <> null && attribute.InformationalVersion <> null then
let value = attribute.InformationalVersion
let index = value.IndexOf(buildVersionMetadataPrefix)
if index > 0 then
let value = value.Substring(index + buildVersionMetadataPrefix.Length)
let success, timestamp = DateTime.TryParseExact(value, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None)
if success then
Some timestamp
else
None
else
None
else
None
Is there a way to get rid of all the 'else None' statements to have only one?
On one side, I can imagine that for some people the code is more clear with all the None statements spelled out, but on the other side, coming from the C world, I see it as clutter that reduces readability.
There are many cases where you need a series of conditions to be met and all the failed cases go to one place.
If I have a list of conditions that depend on each others' success, how can I make a concise short exit without duplication.
Upvotes: 6
Views: 1278
Reputation: 3926
There is an approach that I really love which is the use of an array in certain scenarios.
Example:
Instead of using something like:
if (grade >= 90) {
scale = "A";
} else if (grade >= 80) {
scale = "B";
} else if (grade >= 70) {
scale = "C";
} else if (grade >= 60) {
scale = "D";
} else {
scale = "F";
}
Use an array like:
function calculate(scores) {
var grade, scale;
let sum = 0;
for (let i = 0; i < scores.length; i++) {
sum += scores[i];
}
grade = sum / scores.length;
scale = {
[90 <= grade && grade <= 100]: "O",
[80 <= grade && grade < 90]: "E",
[70 <= grade && grade < 80]: "A",
[55 <= grade && grade < 70]: "P",
[40 <= grade && grade < 55]: "D",
[grade < 40]: "T"
};
console.log(scale.true);
}
In python could be like:
def calculate(scores: list) -> str:
grade = sum(scores) / len(scores)
print(grade)
scale = {90 <= grade <= 100: "O", 80 <=
grade < 90: "E", 70 <= grade < 80: "A",
55 <= grade < 70: "P", 40 <= grade < 55: "D",
grade < 40: "T"}
return scale.get(True)
Upvotes: 0
Reputation:
Have you considered using Result<TSuccess, TError>
. It is very structuring - making the code rigid and flat - and makes it possible to provide detailed error information for the step that possible fails. It's a little more code, but IMO more readable and maintainable:
let getBuildDate (assembly: Assembly) : Result<DateTime, string> =
let buildVersionMetadataPrefix = "+build"
let extractAttribute (assem: Assembly) =
match assem.GetCustomAttribute<AssemblyInformationalVersionAttribute>() with
| attrib when attrib <> null -> Ok attrib
| _ -> Error "No attribute found"
let extractDateString (attrib: AssemblyInformationalVersionAttribute) =
match attrib.InformationalVersion.IndexOf (buildVersionMetadataPrefix) with
| x when x > 0 -> Ok (attrib.InformationalVersion.Substring (x + buildVersionMetadataPrefix.Length))
| _ -> Error "Metadata prefix not found"
let toDateTime dateString =
match DateTime.TryParseExact(dateString, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None) with
| true, timeStamp -> Ok timeStamp
| false, _ -> Error "Invalid date time format"
extractAttribute assembly
|> Result.bind extractDateString
|> Result.bind toDateTime
Usage
let optBuildDate = getBuildDate (Assembly.GetExecutingAssembly())
match optBuildDate with
| Ok date -> printfn "%A" date
| Error msg -> printfn "ERROR: %s" msg
Upvotes: 2
Reputation: 243041
The other answers show how to do this using more sophisticated functional programming methods, like using computation expressions or option
values. Those are definitely useful and make sense if this is something that you are doing in many places throughout your system.
However, if you just want a simple way to change the code so that the control flow is more clear (without making it more clever), I would negate the conditions. Previously, you had:
if something then
moreStuff()
Some result
else
None
You can rewrite this by returning None
if not something
. I think the F# coding convention in this case also allows you to remove the indentation, so it looks more like imperative early return:
if not something then None else
moreStuff()
Some result
With this, you can write your original function as follows - without any extra clever tricks:
let getBuildDate (assembly: Assembly) : DateTime option =
let buildVersionMetadataPrefix = "+build"
let attribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
if attribute = null || attribute.InformationalVersion = null then None else
let value = attribute.InformationalVersion
let index = value.IndexOf(buildVersionMetadataPrefix)
if index <= 0 then None else
let value = value.Substring(index + buildVersionMetadataPrefix.Length)
let success, timestamp = DateTime.TryParseExact(value, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None)
if not success then None else
Some timestamp
Upvotes: 6
Reputation: 4062
open System
open System.Reflection
open System.Globalization
let inline guard cond next = if cond then next () else None
let getBuildDate (assembly: Assembly) : DateTime option =
let buildVersionMetadataPrefix = "+build"
let attribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
guard (attribute <> null && attribute.InformationalVersion <> null) <| fun _ ->
let value = attribute.InformationalVersion
let index = value.IndexOf(buildVersionMetadataPrefix)
guard (index > 0) <| fun _ ->
let value = value.Substring(index + buildVersionMetadataPrefix.Length)
let success, timestamp = DateTime.TryParseExact(value, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None)
guard success <| fun _ ->
Some timestamp
If you can stomach the inelegance of having to write <| fun _ ->
on every guard
, this is an option worth considering.
Upvotes: 1
Reputation: 12667
A readable approach might be use a computation expression builder for Option
.
type OptionBuilder() =
member _.Return v = Some v
member _.Zero () = None
member _.Bind(v, f) = Option.bind f v
member _.ReturnFrom o = o
let opt = OptionBuilder()
You can simulate an imperative style of if-then-return
.
let condition num = num % 2 = 0
let result = opt {
if condition 2 then
if condition 4 then
if condition 6 then
return 10
}
Rewriting your example:
let getBuildDate (assembly: Assembly) : DateTime option = opt {
let buildVersionMetadataPrefix = "+build"
let attribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
if attribute <> null && attribute.InformationalVersion <> null then
let value = attribute.InformationalVersion
let index = value.IndexOf(buildVersionMetadataPrefix)
if index > 0 then
let value = value.Substring(index + buildVersionMetadataPrefix.Length)
let success, timestamp = DateTime.TryParseExact(value, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None)
if success then
return timestamp
}
No more None
.
Upvotes: 3
Reputation: 26213
Another approach might be to use the Option
functions - each of these steps will effectively short circuit if the input from the previous step is None
.
let getBuildDate (assembly: Assembly) : DateTime option =
let tryDate value =
match DateTime.TryParseExact(value, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.None) with
| true, date -> Some date
| false, _ -> None
let buildVersionMetadataPrefix = "+build"
let attribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
Option.ofObj attribute
|> Option.bind (fun attr -> Option.ofObj attr.InformationalVersion)
|> Option.map (fun infVer -> infVer, infVer.IndexOf buildVersionMetadataPrefix)
|> Option.filter (fun (_, index) -> index > 0)
|> Option.map (fun (infVer, index) -> infVer.Substring(index + buildVersionMetadataPrefix.Length))
|> Option.bind tryDate
Whether this is 'better' is arguable - and definitely a matter of opinion!
Upvotes: 10