patternMinor
Small console app to execute some remote scripts
Viewed 0 times
scriptsappsmallsomeremoteconsoleexecute
Problem
I'm learning F# and functional programming, from a background in C# and imperative/OOP. I've ported a small, one-off console app from C# to F#. The port worked (the app behaves the same way), but I'd love to get some feedback on writing more stylistic F# and thinking about code in a functional way, as this was a port of imperative code. I feel like I'm still on the second square of the diagram in this article, or imperative F# code. I don't have a good grasp of code style or any of those basic things yet either!
The console app is intended to read JSON (saved in .txt files) as HTTP POST payloads and send them to a configurable endpoint. I've tried to leave comments in my code where I have uncertainties.
Program.fs:
Functions.fs:
```
namespace MyApp
// In C# I'm used to having my "using" statements inside a namespace, but before a class... Is that idiomatic in F# w/r/t modules/types?
open Newtonsoft.Json
open System.IO
open System.Linq
open RestSharp
open System.Net
open System.Reflection
module Functions =
exception FailedScript of string
// Is it correct/appropriate to restrict method parameter types like this?
// This function loads a .json file with various configurable settings
let loadEnvironmentConfig(path : string) =
let configPath = ".\\" + path + ".json"
Linq.JObject.Parse(File.ReadAllText(configPath))
// The way I've written this method it takes a tuple of strings, right?
// Is it idiomatic to 'bundle' method params in this way?
// This function is intended to navigate up the directory hierarchy until it finds one with the provided name
let rec findDirectoryFrom(path : string, name : string) =
// Good to invoke the ctor without parens?
let parent = DirectoryInfo path
// Is there a more F#-y wa
The console app is intended to read JSON (saved in .txt files) as HTTP POST payloads and send them to a configurable endpoint. I've tried to leave comments in my code where I have uncertainties.
Program.fs:
open MyApp.Functions
[]
let main argv =
let environment = argv.GetValue(0).ToString()
let config = loadEnvironmentConfig(environment)
executeScripts(config)
0Functions.fs:
```
namespace MyApp
// In C# I'm used to having my "using" statements inside a namespace, but before a class... Is that idiomatic in F# w/r/t modules/types?
open Newtonsoft.Json
open System.IO
open System.Linq
open RestSharp
open System.Net
open System.Reflection
module Functions =
exception FailedScript of string
// Is it correct/appropriate to restrict method parameter types like this?
// This function loads a .json file with various configurable settings
let loadEnvironmentConfig(path : string) =
let configPath = ".\\" + path + ".json"
Linq.JObject.Parse(File.ReadAllText(configPath))
// The way I've written this method it takes a tuple of strings, right?
// Is it idiomatic to 'bundle' method params in this way?
// This function is intended to navigate up the directory hierarchy until it finds one with the provided name
let rec findDirectoryFrom(path : string, name : string) =
// Good to invoke the ctor without parens?
let parent = DirectoryInfo path
// Is there a more F#-y wa
Solution
Welcome to the wonderful world of F#!
You're doing fine so far! You'll find that the code stops being quite so awkward as you get more experienced.
Meanwhile, here are my comments:
Type annotations
You'll find that in many cases, F# can infer the types for you. So you can write your first two functions like this:
Annotations are normally required for OO style code. When you do
Pattern matching
It's unidiomatic to use
Instead, convert to a list and use pattern matching. Unlike
Here's another example of pattern matching:
Use the native F# collection functions, such as map, rather than Linq
Note that I am using piping to pass the data through each step
Local helper methods
And I like to create local helper methods like
Nested collections
You can use the
Type constraints
You can use the
In this case, you don't even need this constraint -- you could just use the base
Removing string types
The
Changing parameter order to make partial application easier
The
I also reversed the parameters for
Creating helper types
It's very easy to create "little" types in a few lines. I like to do this to make the code more self-explanatory.
For example, in your code, you access three things from the config. Why not put that in a type to make it more obvious what you are using the config for?
Watch out for ignore
When you see messages about "should be a unit", don't just use
In this case, the code below is probably buggy. You are comparing the two values for equality rather than doing assignment!
I think what you wanted was to assign the value like this:
Setting properties in the constructor
F# allows you to pass properties in the constructor, which eliminates a lot of assignment ugliness. So you can write something like this:
```
let authenticator = HttpBasicAuthenticator(c
You're doing fine so far! You'll find that the code stops being quite so awkward as you get more experienced.
Meanwhile, here are my comments:
Type annotations
You'll find that in many cases, F# can infer the types for you. So you can write your first two functions like this:
let loadEnvironmentConfig path =
let configPath = ".\\" + path + ".json"
Linq.JObject.Parse(File.ReadAllText(configPath))
let rec findDirectoryFrom path name =Annotations are normally required for OO style code. When you do
x.Length the compiler has no idea what x is, so you have to annotate with string or whatever.Pattern matching
It's unidiomatic to use
FirstOrDefault and then check for null, especially as F# types can't be null!Instead, convert to a list and use pattern matching. Unlike
FirstOrDefault, you can never accidentally forget to handle the empty list case.let rec findDirectoryFrom path name =
let parent = DirectoryInfo path
let possibleMatches = parent.GetDirectories(name) |> List.ofArray
match possibleMatches with
// none
| [] -> findDirectoryFrom parent.Parent.FullName name
// one or more. Return the first. Ignore the others
| possibleMatch::_ -> possibleMatchHere's another example of pattern matching:
let response = client.Execute(request)
match response.StatusCode with
| HttpStatusCode.Conflict
| HttpStatusCode.Created -> response.StatusCode
| _ -> raise (FailedScript(response.ErrorMessage))Use the native F# collection functions, such as map, rather than Linq
// use "map" rather than Linq.Select
let scripts =
directory.EnumerateFiles("*.txt")
|> Array.map deserializeFileNote that I am using piping to pass the data through each step
Local helper methods
And I like to create local helper methods like
deserializeFile to make the main flow of code tidier:// create a helper
let deserializeFile (fi:FileInfo) =
JsonConvert.DeserializeObject(File.ReadAllText(fi.FullName))Nested collections
You can use the
collect function to collapse nested collections. It is similar to Linq.SelectManylet childScripts =
directory.GetDirectories()
|> Seq.collect getScriptsFromDirectoryType constraints
You can use the
#ParentClass to force a subclass type constraint:let executeScript script (client:#RestClient) =In this case, you don't even need this constraint -- you could just use the base
RestClient.Removing string types
The
findDirectoryFrom takes two strings as parameters, so it would be easy to accidentally mix them up. I would make the path be a DirectoryInfo instead:let rec findDirectoryFrom2 name (parent:DirectoryInfo) =
let possibleMatches = parent.GetDirectories(name) |> List.ofArray
match possibleMatches with
// none
| [] -> findDirectoryFrom2 name parent.Parent
// one or more. Return the first. Ignore the others
| possibleMatch::_ -> possibleMatchChanging parameter order to make partial application easier
The
findDirectoryFrom2 also has the parent as the second parameter now, which makes it easier to pipe a directory into it, so you can write code in a "pipeline" approach like this:let executingPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
DirectoryInfo executingPath
// find the scripts directory
|> findDirectoryFrom2 "scripts"
// load all the the scripts in that directory
|> getScriptsFromDirectoryI also reversed the parameters for
executeScript for this reason:let executeScript (client:RestClient) script =Creating helper types
It's very easy to create "little" types in a few lines. I like to do this to make the code more self-explanatory.
For example, in your code, you access three things from the config. Why not put that in a type to make it more obvious what you are using the config for?
type ConfigInfo = {
Endpoint: string
UserName: string
Password : string
}
let getConfigInfo (config : Linq.JObject) =
let endpoint = config.GetValue("DeploymentApiEndpoint").ToString()
let username = config.GetValue("UserName").ToString()
let password = config.GetValue("Password").ToString()
{ Endpoint = endpoint; UserName = username; Password = password }Watch out for ignore
When you see messages about "should be a unit", don't just use
ignore without understanding the error.In this case, the code below is probably buggy. You are comparing the two values for equality rather than doing assignment!
client.Authenticator = authenticator |> ignoreI think what you wanted was to assign the value like this:
client.Authenticator <- authenticatorSetting properties in the constructor
F# allows you to pass properties in the constructor, which eliminates a lot of assignment ugliness. So you can write something like this:
```
let authenticator = HttpBasicAuthenticator(c
Code Snippets
let loadEnvironmentConfig path =
let configPath = ".\\" + path + ".json"
Linq.JObject.Parse(File.ReadAllText(configPath))
let rec findDirectoryFrom path name =let rec findDirectoryFrom path name =
let parent = DirectoryInfo path
let possibleMatches = parent.GetDirectories(name) |> List.ofArray
match possibleMatches with
// none
| [] -> findDirectoryFrom parent.Parent.FullName name
// one or more. Return the first. Ignore the others
| possibleMatch::_ -> possibleMatchlet response = client.Execute(request)
match response.StatusCode with
| HttpStatusCode.Conflict
| HttpStatusCode.Created -> response.StatusCode
| _ -> raise (FailedScript(response.ErrorMessage))// use "map" rather than Linq.Select
let scripts =
directory.EnumerateFiles("*.txt")
|> Array.map deserializeFile// create a helper
let deserializeFile (fi:FileInfo) =
JsonConvert.DeserializeObject(File.ReadAllText(fi.FullName))Context
StackExchange Code Review Q#86083, answer score: 5
Revisions (0)
No revisions yet.