4

Before you leave: though my code is in F#, this question is applicable to any .NET langague.

So here's my situation -- I have a simple ProgramOptions record in F# for holding command line option data. Each field represents a distinct option, and they can have default values, which are marked with a custom attribute.

type ProgramOptionAttribute(defaultValue: obj) =
    inherit Attribute()

type ProgramOptions =
    { [<ProgramOption("render.pdf")>] output: string
      [<ProgramOption(true)>] printOutput: bool
      // several more
      }

I've written a nice little function elsewhere to dynamically instantiate this record using the attribute data, given the raw command line options. That works great. But it's really easy to introduce a runtime type mismatch by providing an object to the attribute that is a different type from the field (since there's just a simple cast from obj -> field type later down the line). For example:

// Obviously wrong, but compiles without complaint, failing at runtime
[<ProgramOption(42)>] myField: bool

Is there a way to make that type safe? Some kind of generic trickery? Or is what I want impossible?

Jwosty
  • 3,497
  • 2
  • 22
  • 50
  • 1
    I don't have time to write a proper answer, but take a look [at this SO question](http://stackoverflow.com/questions/294216/why-does-c-sharp-forbid-generic-attribute-types). Granted that is a bit older question, but it doesn't appear that what you want is possible with generics. The comments there also suggest that maybe you can do some things with `typeof`, etc. But even with that I don't believe you will be able to get a compile error like you want. – Becuzz Aug 19 '15 at 20:43

2 Answers2

4

As far as I know, that's not possible, but even if it was, what would it gain you? You can only use literals in attributes, so you'd be constrained to string, int, bool, and a few other types.

What if, at some day, you'd want to define a record with a DateTimeOffset value in it (not an unreasonable case)?

type myRecord = {
    [<ProgramOption(???)>]Date : DateTimeOffset
    [<ProgramOption("foo")>]Text : string }

Which ProgramOption would you put on the Date element?

That said, you don't necessarily have to come up with you own attribute. The BCL already defines a [<DefaultValue>] attribute, as well as lots of so-called data annotations attributes. None of them are, in my opinion, useful for anything. As an example, here's an in-depth explanation of why the Required attribute is redundant. That article is about Object-Oriented Design, but applies to Functional Programming as well.

In a statically typed Functional language like F#, you should make illegal states unrepresentable.

Ultimately, I think that a simpler approach would be to define a default value for each type, like this:

type ProgramOptions = {
    Output: string
    PrintOutput: bool
    // several more
    }

let defaultProgramOptions = { Output = "render.pdf"; PrintOutput = true }

This would let you easily create values based on the default value:

let myProgOps = { defaultProgramOptions with PrintOutput = false }

This is type-safe at compile-time, and produces a ProgramOptions value with these constituent elements:

{ Output = "render.pdf"; PrintOutput = false; }
Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • This was actually my first approach -- however, the main reason I'm trying to use attributes is because it is eliminating duplication. Without a dynamically created record, I have to specify the name of each program option several times, and you have to add several lines to add another one. So I guess it comes down to deciding if I want type safety or less code duplication... – Jwosty Aug 20 '15 at 15:53
0

I am not sure this is possible at compile time, but during runtime I convert numeric values between types, when it is possible, or convert all values to string. A snippet from a larger program:

// set default values and parameters from attributes
    // this will override explicit calls to DefineParameters
    let props = 
      ty
          .GetProperties(BindingFlags.Public ||| BindingFlags.NonPublic ||| BindingFlags.Instance)
          .Where(fun p -> 
                  p.GetCustomAttributes<ParameterAttribute>(false) //.Count() > 0)
                    .SingleOrDefault(fun attr -> attr.UnitPeriod = OptionalValue.Missing || attr.UnitPeriod = OptionalValue(this.BarUnitPeriod)) <> Unchecked.defaultof<ParameterAttribute>)

    for p in props do
      let attr = p.GetCustomAttributes<ParameterAttribute>(false).SingleOrDefault(fun attr -> attr.UnitPeriod = OptionalValue.Missing || attr.UnitPeriod = OptionalValue(this.BarUnitPeriod)) //:?> ParameterAttribute , typeof<ParameterAttribute>
      if attr.IsString then
        this.DefineParameter(attr.Code, attr.DefaultValue :?> string, attr.Description)
      else
        this.DefineParameter(attr.Code, attr.DefaultValue :?> float, attr.Min, attr.Max, attr.Step, attr.Description)
        optPropInfos.Add(attr.Code, p)
      let value = this.GetParam(attr.Code) // this function gets a value from an attribute
      match p.PropertyType with
      | x when x = typeof<Int32> -> p.SetValue(this, Convert.ToInt32(value), null)
      | x when x = typeof<Int64> -> p.SetValue(this, Convert.ToInt64(value), null)
      | x when x = typeof<decimal> -> p.SetValue(this, Convert.ToDecimal(value), null)
      | x when x = typeof<float> -> p.SetValue(this, Convert.ToDouble(value), null)
      | x when x = typeof<string> -> p.SetValue(this, Convert.ToString(value), null)
      | _ -> failwith "ParameterAttribute could be applied to Int32/64, decimal, float or string types only"
V.B.
  • 6,236
  • 1
  • 33
  • 56