-
Notifications
You must be signed in to change notification settings - Fork 21
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
Allow Record Option
fields to be... optional
#918
Comments
Option
fields to be... optional
Sorry! I did an awful job at looking 🤦 I must say, I only want to cover the |
type SomeConfig =
{
Name: string
Description: string option
Tags: string list option
}
type SomeOtherConfig =
{
Name: string
Tags: string list option
Foo: string option
}
// What should this be?
let x = { Name = "James"; Tags = [ "a"; "b" ] } |
I would like this change but not as a language feature, but rather a library. There are different mechanisms to achieve it, either macros, type provider consuming types. |
Just seems like any other case where the compiler can't infer, and you have to add an annotation. let x : SomeConfig = { Name = "James"; Tags = [ "a"; "b" ] }
let x = { SomeConfig.Name = "James"; Tags = [ "a"; "b" ] } |
The same as type SomeConfig = { Name: string }
type SomeOtherConfig = { Name: string }
// What should this be?
let x = { Name = "James" }
do System.Console.WriteLine(x.GetType()) |
I often find myself writing lots of code for "Zero" or "Default" types: type Foo =
{
Foo : int
}
with
static member Zero
with get () = { Foo = 0 }
type Bar =
{
Bar : list int
}
with
static member Zero
with get () = { Bar = [] }
type Baz =
{
Foo : Foo
Bar : Bar
Qux : string option
}
with
static member Zero
with get () =
{
Foo = Foo.Zero
Bar = Bar.Zero
Qux = None
} Currently it feels like a limitation of F# that I must manually write out these static members. However, I'm not sure what language feature could make this more convenient. I am not convinced that I think this is also related to type-class proposals, since |
Yeah, I use a lot of While I've been implementing a DSL for configuration of different kinds (which renders out to json), such as Kuberenetes resources, and I've found the best methods for a user-friendly API are:
I just think it'd be a whole lot simpler if we could make fields optional like a static method. |
You don't need a computation expression here, just a regular static method leveraging named optional arguments.
I think you could probably even have Myriad generate the boilerplate Create method for you. (Hat tip to @voronoipotato) |
I totally agree that's an option - and I leverage this as an alternate - but the computation expression DSL I'm leveraging in my library is clearer. (This idea was taken pretty much wholesale from https://github.com/UnoSD/Pulumi.FSharp.Extensions) Take the below: prometheusRule {
metadata {
name "elasticsearch-monitor"
}
spec {
serviceMonitorSpec {
jobLabel "elasticsearch"
namespaceSelector {
matchNames [ "elasticsearch" ]
}
endpoints {
// CE's also provide some nice behaviour via `yield`. In this case I'm 'inheriting' from a default
Config.defaultEndpoints
port "http-metrics"
// Implicitly added to a list
metricRelabeling {
action Replace
sourceLabels [ "host" ]
regex "(.*)"
targetLabel "pod_ip"
}
// Again, using `yield` and custom functions I can create boilerplate-removing helpers
// (These do the same as the above `metricRelabling` block)
dropLabel "pod"
dropLabel "endpoint"
}
}
}
}
vs. // I know this could be streamlined a little
PrometheusRule.create (
metadata = Metadata.create (
name = "elasticsearch-monitor"
),
spec = Spec.create (
ServiceMonitorSpec.create (
jobLabel = "elasticsearch",
namespaceSelector = NamespaceSelector.create (
matchNames = [ "elasticsearch" ]
),
endpoints = Endpoints.create (
port = "http-metrics"
metricRelabelings = [
MetricRelabeling.create (
action = Replace,
sourceLabels = [ "host" ],
regex = "(.*)",
targetLabel = "pod_ip"
)
]
/* More stuff */
)
)
)
) I realise it might not look too different, but the idea is a clear DSL that allows you to build complicated, nested records I know we've diverged from the topic at hand a little, but if you squint hard enough at the first example, you can see it looks more like a record than the static methods, and I think it'd be nice to mirror that kind of structure a little with the record literal syntax - sure it means you don't get all of the yield overloading, but also means a whole lot less work for an underlying library. Now, if #920 wasn't an issue, I'd be able to leverage custom operations instead of functions (to limit their scope). |
Closing as #617 covers this |
After a brief discussion in Slack, I thought it'd bring it up here!
I've been toying around replacing some cumbersome YAML configs and F# has been great so far due to its type-safety, terse syntax and ability to provide more than just static configuration.
But one minor annoyance is the case of a record with
Option
fields. Ideally, I'd like to be able to:Some
value without theSome
(it's implicit)None
Obviously, this would be opt-in, possibly via. an annotation on the entire record, or the field, or prefixing a field name with a
?
, similar to the POCO style (eg.?Name
).For example:
The existing way of approaching this problem in F# is to...
Use a POCO type and leverage the in-built optional fields syntax (eg.
?description: string
) - This means you're either using a POCO now, or writing another type to generate your record, and not leveraging the awesome record initialisation syntax.Use a computation expression and mock the entire record initialisation syntax using custom operations - Cumbersome to implement, especially for large records, but provides the most friendly API for the consumer.
Create a 'default' version of the record that already has all optional fields filled with a
None
value. This has the down-side that this the mandatory fields still need to be populated somehow, eg.This approach works fine if only a few fields are mandatory and the rest are optional. If all the fields are optional, then the 'default' can be all
None
s, hurray!But this soon becomes a problem in itself when many fields are optional and many are mandatory, and might lead to using a 'SomeConfigMandatoryFields`/anonymous record to fill in the blanks.
Pros and Cons
We already have this concept available for POCOs, and if there was some way to implement this for the record initialisation syntax, I feel like it'd be a great extension to writing custom APIs.
I've looked at Dhall (Haskell based configuration langauge) which suffers from the same issues, and I've been using jsonnet for a while and in all honesty, it's an awful experience. I'd much rather leverage F#'s friendly, familiar syntax and all of the power dotnet behind it.
Currently, I'm using computation expressions to bridge this gap, but it does mean a whole lot of extra work for large records and the syntax could be confusing at first if somebody is just expecting a record (It's kinda overkill just to create a record).
The disadvantages of making this adjustment to F# are it being unclear at first glance when looking at records created in such a way. This could be remedied by some information in the IDE popup?
Extra information
Estimated cost (XS, S, M, L, XL, XXL): I couldn't say in all honesty 😬
Related suggestions: (put links to related suggestions here)
Affidavit (please submit!)
Please tick this by placing a cross in the box:
Please tick all that apply:
For Readers
If you would like to see this issue implemented, please click the 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.
The text was updated successfully, but these errors were encountered: