37

in C# you can do stuff like :

var a = new {name = "cow", sound = "moooo", omg = "wtfbbq"};

and in Python you can do stuff like

a = t(name = "cow", sound = "moooo", omg = "wtfbbq")

Not by default, of course, but it's trivial to implement a class t that lets you do it. Infact I did exactly that when I was working with Python and found it incredibly handy for small throwaway containers where you want to be able to access the components by name rather than by index (which is easy to mix up).

Other than that detail, they are basically identical to tuples in the niche they serve.

In particular, I'm looking at this C# code now:

routes.MapRoute(
            "Default", // Route name
            "{controller}/{action}/{id}", // URL with parameters
            new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
        );

and it's F# equivalent

type Route = { 
    controller : string
    action : string
    id : UrlParameter }

routes.MapRoute(
    "Default", // Route name
    "{controller}/{action}/{id}", // URL with parameters
    { controller = "Home"; action = "Index"; id = UrlParameter.Optional } // Parameter defaults
  )

Which is both verbose and repetitive, not to mention rather annoying. How close can you get to this sort of syntax in F#? I don't mind jumping through some hoops (even flaming hoops!) now if it means it'll give me something useful to DRY up code like this.

ildjarn
  • 62,044
  • 9
  • 127
  • 211
Li Haoyi
  • 15,330
  • 17
  • 80
  • 137
  • 4
    Just to mention that anonymous types are not tuples in c# there are the Tuple classes – Ben Robinson Nov 15 '11 at 22:46
  • F# 4.6 preview: Anonymous Records https://github.com/fsharp/fslang-design/blob/master/FSharp-4.6/FS-1030-anonymous-records.md – Tony Jan 23 '19 at 23:21

6 Answers6

26

I find it easier to do

let route = routes.MapRoute(
    "Default", // Route name
    "{controller}/{action}/{id}" // URL with parameters
    )
route.Defaults.Add("controller", "Home")
route.Defaults.Add("action", "Index")

or

[ "controller", "Home"
  "action", "Index" ]
|> List.iter route.Defaults.Add

In F#, I would avoid calling overloads that accept anonymous types much as I would avoid calling an F# method accepting FSharpList from C#. Those are language-specific features. Usually there is a language-agnostic overload/workaround available.

EDIT

Just looked at the docs--here's yet another way to do it

let inline (=>) a b = a, box b

let defaults = dict [
  "controller" => "Home"
  "action"     => "Index" 
]
route.Defaults <- RouteValueDictionary(defaults)
Daniel
  • 47,404
  • 11
  • 101
  • 179
  • 1
    +1 This is definitely nicer. I didn't know that there is a more sensible overload for `Add` - I should use that in my ASP.NET MVC samples too! – Tomas Petricek Nov 15 '11 at 22:56
  • Re: using RouteValueDictionary. This compiles, but doesn't work, because the expected dictionary type is IDictionary. At runtime it unboxes the "obj", so it must first be box'ed when it is created. Instead, use: "dict [ ("controller", box "Home");("action", box "Index")]". This also enables defaults of mixed types, eg. ("User", box null),("id", box UrlParameter.Optional). – Stephen Hosking Nov 17 '11 at 01:06
  • 1
    @Daniel. Nice work. I like the (=>) operator. Makes it much more readable. – Stephen Hosking Nov 18 '11 at 06:28
18

You can't create "anonymous records" in F# - when using types, you can either use tuples which are anonymous, but don't carry labels or you can use records which have to be declared in advance and have labels:

// Creating an anonymous tuple
let route = ("Home", "Index", UrlParameter.Optional)

// Declaration and creating of a record with named fields
type Route = { controller : string; action : string; id : UrlParameter } 
let route = { controller = "Home"; action = "Index"; id = UrlParameter.Optional } 

Technically, the problem with anonymous records is that they would have to be defined as actual classes somewhere (the .NET runtime needs a type), but if the compiler put them in every assembly, then two anonymous records with same members might be different types if they were defined in different assemblies.

Honestly, I think that the example you posted is just a poor design decision in ASP.NET - it is misusing a particular C# feature to do something for which it wasn't designed. It may not be as bad as this, but it's still odd. The library takes a C# anonymous type, but it uses it as a dictionary (i.e. it uses it just as a nice way to create key-value pairs, because the properties that you need to specify are dynamic).

So, if you're using ASP.NET from F#, it is probably easier to use an alternative approach where you don't have to create records - if the ASP.NET API provides some alternative (As Daniel shows, there is a nicer way to write that).

Community
  • 1
  • 1
Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • It's a poor design decision for F# programmers. In C#, I am curious what alternative you think would have been as concise and clear. – Kirk Woll Nov 15 '11 at 22:55
  • 1
    @Kirk: An `IDictionary` seems like a better choice. In F# that would be `dict ["controller", "Home"; "action", "Index"]` -- very natural. – Daniel Nov 15 '11 at 22:58
  • 1
    I think that would actually be neither as concise nor as clear: `new Dictionary { { "controller", "Home" }, { "action", "Index" } }` vs. `new { controller = "Home", action = "Index" }` (And was specifically asking about a solution that wouldn't be worse in C#) – Kirk Woll Nov 15 '11 at 23:02
  • 3
    I wouldn't say `IDictionary` is clearer or shorter in C#, but it's a better approach (happy medium) given that several languages target the CLR. Anonymous types are specific to C#. – Daniel Nov 15 '11 at 23:07
  • 1
    The [F# Component Guidelines](http://research.microsoft.com/en-us/um/cambridge/projects/fsharp/manual/fsharp-component-design-guidelines.pdf) has a section specific to making APIs friendly to other .NET languages. Now that F# is a first-class language, perhaps the same thought should be applied to making the BCL (and C# APIs in general) friendly to F#. – Daniel Nov 15 '11 at 23:13
  • 6
    The point is, if ASP.NET is a .NET library (as opposed to being a C# library), it simply shouldn't make any assumptions about the language that people are going to use. Using classes with properties to represent dictionaries sounds pretty silly from the pure .NET perspective! – Tomas Petricek Nov 15 '11 at 23:15
  • @Tomas: That sums it up well! – Daniel Nov 15 '11 at 23:18
  • The syntax available in a language often goes a long way towards what design will work best for a given API. For example, Java has no lambdas (or even anonymous methods) and so this makes it a poor language choice for using a Linq-style method-chain API as we have in .Net (`.Where` et al becomes prohibitively verbose). I'm pretty sure if F# had as a language feature "anonymous records" we would not be having this discussion. – Kirk Woll Nov 15 '11 at 23:35
  • With regard to the "two anonymous records with same members might be different types if they were defined in different assemblies": why doesn't C# have this problem? (or does it?) – Li Haoyi Nov 16 '11 at 00:18
  • 1
    @LiHaoyi : C# doesn't generally allow anonymous types to escape local scope. ASP.Net only works with anonymous types because it uses reflection under the hood. – ildjarn Nov 16 '11 at 00:23
  • @Kirk : The larger point is that .NET libraries (i.e., those intended to be used from _any_ .NET language) should not use non-[CLS-compliant](http://msdn.microsoft.com/en-us/library/12a7a7h3.aspx) constructs. The .NET design guidelines themselves go out of their way to state this over and over. Generics and delegates (as needed for lambdas to function) are CLS-compliant, anonymous types are not. – ildjarn Nov 16 '11 at 00:25
  • 7
    @Kirk - even in C#, there are still problems with this approach, such as discoverability. The `defaults` parameter has type `object` with description "An object that contains default route values.". As a caller, this is completely inscrutable unless I've seen examples of the heavily idiomatic style used by the library. To me, this is symptomatic of a workaround for C#'s inability to support concise collection literals, rather than a positive use of a C# feature that F# lacks. – kvb Nov 16 '11 at 01:57
  • Personally I think it's already an ugly hack in C#. The clean way would have been a `params` equivalent for named parameters. – CodesInChaos Nov 16 '11 at 11:09
12

Now in F# 4.6 (preview) we have Anonymous Records

So we can have this code syntax:

let AwesomeAnonymous = {|  ID = Guid.NewGuid()
                           Name = "F#"
                        |}

AwesomeAnonymous.Name |> Debug.WriteLine

It is also supported on the Visual Studio Intellisense: Screenshot

So that code could be like this:

routes.MapRoute(
    "Default", // Route name
    "{controller}/{action}/{id}", // URL with parameters
    {| controller = "Home"; action = "Index"; id = UrlParameter.Optional |} // Parameter defaults
  )

See also: Announcing F# 4.6 Preview

Tony
  • 16,527
  • 15
  • 80
  • 134
11

The OP does not describe the best use of anonymous type. They are best used when using LINQ to map to an arbitrary class. For example:

var results = context.Students
              .Where(x => x.CourseID = 12)
              .Select(x => new { 
                 StudentID = x.ID, 
                 Name = x.Forename + " " + x.Surname
              });

I know this can be done by defining a new record type, but then you have two places to maintain code, (1) the record type definition (2) where you've used it.

It could instead be done with a tuple, but to access individual fields you have to use the deconstruction syntax (studentId, name) all the time. This becomes unwieldy if you have 5 items in the tuple. I would rather type in x and hit dot and have intellisense tell me what fields are available.

Tahir Hassan
  • 5,715
  • 6
  • 45
  • 65
2

Here's my take on the default web project route config:

module RouteConfig =

    open System.Web.Mvc
    open System.Web.Routing

    let registerRoutes (routes: RouteCollection) =

        routes.IgnoreRoute("{resource}.axd/{*pathInfo}")

        /// create a pair, boxing the second item
        let inline (=>) a b = a, box b

        /// set the Defaults property from a given dictionary
        let setDefaults defaultDict (route : Route) =  
            route.Defaults <- RouteValueDictionary(defaultDict)

        routes.MapRoute(name="Default", url="{controller}/{action}/{id}")
        |> setDefaults (dict ["controller" => "Home" 
                              "action" => "Index" 
                              "id" => UrlParameter.Optional])
Christopher Stevenson
  • 2,843
  • 20
  • 25
2

As Tony indicated in his answer this is not much better as of F# 4.6. Testing a similar example using .NET Core SDK 3.0.100-preview4-011158 I was able to demonstrate use of the new Anonymous Record feature. As for the RouteMap method, I'm unfamiliar with what types of values this API accepts but I would suspect that the example below would work.

Ex.

routes.MapRoute(
    "Default", // Route name
    "{controller}/{action}/{id}", // URL with parameters
    {| controller = "Home"; action = "Index"; id = UrlParameter.Optional |} // Parameter defaults
  )

Notice the use of the | character on the insides of the curly braces. This is what now distinguishes regular records from anonymous records in F#.

As for your other example, perhaps the F# example would now look such as below.

let a = {| name = "cow"; sound = "moooo"; omg = "wtfbbq" |}
jpierson
  • 16,435
  • 14
  • 105
  • 149