F# Add Constructor to a Record?
-
26-09-2019 - |
Question
Basically I want to have a single construct to deal with serializing to both JSON and formatted xml. Records worked nicely for serializing to/from json. However XmlSerializer requires a parameterless construtor. I don't really want to have to go through the exercise of building class objects for these constructs (principle only). I was hoping there could be some shortcut for getting a parameterless constructor onto a record (perhaps with a wioth statement or something). I can't get it to behave - has anybody in the community had any luck?
module JSONExample
open System
open System.IO
open System.Net
open System.Text
open System.Web
open System.Xml
open System.Security.Authentication
open System.Runtime.Serialization //add assemnbly reference System.Runtime.Serialization System.Xml
open System.Xml.Serialization
open System.Collections.Generic
[<DataContract>]
type ChemicalElementRecord = {
[<XmlAttribute("name")>]
[<field: DataMember(Name="name") >]
Name:string
[<XmlAttribute("name")>]
[<field: DataMember(Name="boiling_point") >]
BoilingPoint:string
[<XmlAttribute("atomic-mass")>]
[<field: DataMember(Name="atomic_mass") >]
AtomicMass:string
}
[<XmlRoot("freebase")>]
[<DataContract>]
type FreebaseResultRecord = {
[<XmlAttribute("code")>]
[<field: DataMember(Name="code") >]
Code:string
[<XmlArrayAttribute("results")>]
[<XmlArrayItem(typeof<ChemicalElementRecord>, ElementName = "chemical-element")>]
[<field: DataMember(Name="result") >]
Result: ChemicalElementRecord array
[<XmlElement("message")>]
[<field: DataMember(Name="message") >]
Message:string
}
let getJsonFromWeb() =
let query = "[{'type':'/chemistry/chemical_element','name':null,'boiling_point':null,'atomic_mass':null}]"
let query = query.Replace("'","\"")
let queryUrl = sprintf "http://api.freebase.com/api/service/mqlread?query=%s" "{\"query\":"+query+"}"
let request : HttpWebRequest = downcast WebRequest.Create(queryUrl)
request.Method <- "GET"
request.ContentType <- "application/x-www-form-urlencoded"
let response = request.GetResponse()
let result =
try
use reader = new StreamReader(response.GetResponseStream())
reader.ReadToEnd();
finally
response.Close()
let data = Encoding.Unicode.GetBytes(result);
let stream = new MemoryStream()
stream.Write(data, 0, data.Length);
stream.Position <- 0L
stream
let test =
// get some JSON from the web
let stream = getJsonFromWeb()
// convert the stream of JSON into an F# Record
let JsonSerializer = Json.DataContractJsonSerializer(typeof<FreebaseResultRecord>)
let result: FreebaseResultRecord = downcast JsonSerializer.ReadObject(stream)
// save the Records to disk as JSON
use fs = new FileStream(@"C:\temp\freebase.json", FileMode.Create)
JsonSerializer.WriteObject(fs,result)
fs.Close()
// save the Records to disk as System Controlled XML
let xmlSerializer = DataContractSerializer(typeof<FreebaseResultRecord>);
use fs = new FileStream(@"C:\temp\freebase.xml", FileMode.Create)
xmlSerializer.WriteObject(fs,result)
fs.Close()
use fs = new FileStream(@"C:\temp\freebase-pretty.xml", FileMode.Create)
let xmlSerializer = XmlSerializer(typeof<FreebaseResultRecord>)
xmlSerializer.Serialize(fs,result)
fs.Close()
ignore(test)
Solution 3
Looks like you can't change a record to a class - or add a edfault constructor to it.
The example provided (basedof off this article: http://blogs.msdn.com/b/jomo_fisher/archive/2010/03/06/neat-sample-f-and-freebase.aspx) gets a stream of json from api.freebase.com; we then deserialize into the attributed classes; next serialize it as Json to disk; then serialize it as Xml to disk (using DataContract); finally, with the best controll of the output serialize it as Xml to disk (using XmlSerializer):
Notes:
DataContract(family of) attributes for DataContractJsonSerializer and JSON.DataContractJsonSerializer - these occur over the class names and the memeber variables. DataContract stuff was straight forward - and works on record types as well.
XmlSerializer(family of) attributes over the class and the Property Getter/Setter. This requires the type is an object with a default constructor, and property Getters and Setters w/ attributes associated with each of them. If a Property doesn't have eitehr a getter or a setter it will not serialize - which was a suprise (I imagined the default onstructor would ensure that the object had defulat values upon deserialization and the setters would update with whatever was serialized - but no this isn't the case).
Another nifty (sigh) thing about XmlSerialization is that the classes can't be contained within a module. So we move the types up to a namespace...
namespace JSONExample
open System
open System.IO
open System.Net
open System.Text
open System.Web
open System.Xml
open System.Security.Authentication
open System.Runtime.Serialization //add assemnbly reference System.Runtime.Serialization System.Xml
open System.Xml.Serialization
open System.Collections.Generic
[<DataContract>]
type ChemicalElementRecord() =
[<field: DataMember(Name="name") >]
let mutable name: string = ""
[<field: DataMember(Name="boiling_point") >]
let mutable boilingPoint: string =""
[<field: DataMember(Name="atomic_mass") >]
let mutable atomicMass: string = ""
[<XmlAttribute("name")>]
member this.Name with get() = name and set v = name <- v
[<XmlAttribute("boiling-point")>]
member this.BoilingPoint with get() = boilingPoint and set v = boilingPoint <- v
[<XmlAttribute("atomic-mass")>]
member this.AtomicMass with get() = atomicMass and set v = atomicMass <- v
[<XmlRoot("freebase")>]
[<DataContract>]
type FreebaseResultRecord() =
[<field: DataMember(Name="code") >]
let mutable code: string = ""
[<field: DataMember(Name="result") >]
let mutable result: ChemicalElementRecord array = Array.empty
[<field: DataMember(Name="message") >]
let mutable message: string = ""
[<XmlElement("message")>]
member this.Message with get() : string = message and set v = message <- v
[<XmlArrayAttribute("chemical-elements")>]
[<XmlArrayItem(typeof<ChemicalElementRecord>, ElementName = "chemical-element")>]
member this.Result with get() = result and set v = result <- v
[<XmlAttribute("code")>]
member this.Code with get() = code and set v = code <- v
module Test =
let getJsonFromWeb() =
let query = "[{'type':'/chemistry/chemical_element','name':null,'boiling_point':null,'atomic_mass':null}]"
let query = query.Replace("'","\"")
let queryUrl = sprintf "http://api.freebase.com/api/service/mqlread?query=%s" "{\"query\":"+query+"}"
let request : HttpWebRequest = downcast WebRequest.Create(queryUrl)
request.Method <- "GET"
request.ContentType <- "application/x-www-form-urlencoded"
let response = request.GetResponse()
let result =
try
use reader = new StreamReader(response.GetResponseStream())
reader.ReadToEnd();
finally
response.Close()
let data = Encoding.Unicode.GetBytes(result);
let stream = new MemoryStream()
stream.Write(data, 0, data.Length);
stream.Position <- 0L
stream
let test =
// get some JSON from the web
let stream = getJsonFromWeb()
// convert the stream of JSON into an F# Record
let JsonSerializer = Json.DataContractJsonSerializer(typeof<FreebaseResultRecord>)
let result: FreebaseResultRecord = downcast JsonSerializer.ReadObject(stream)
// save the Records to disk as JSON
use fs = new FileStream(@"C:\temp\freebase.json", FileMode.Create)
JsonSerializer.WriteObject(fs,result)
fs.Close()
// save the Records to disk as System Controlled XML
let xmlSerializer = DataContractSerializer(typeof<FreebaseResultRecord>);
use fs = new FileStream(@"C:\temp\freebase.xml", FileMode.Create)
xmlSerializer.WriteObject(fs,result)
fs.Close()
use fs = new FileStream(@"C:\temp\freebase-pretty.xml", FileMode.Create)
let xmlSerializer = XmlSerializer(typeof<FreebaseResultRecord>)
xmlSerializer.Serialize(fs,result)
fs.Close()
ignore(test)
OTHER TIPS
Blockquote I was hoping there could be some shortcut for getting a parameterless constructor onto a record
This is possible in F# version 3.0 via use of the CLIMutable attribute. Add this to your record type for a parameterless constructor and read/write properties (instead of read only properties).
Why not use the DataContractSerializer (for Xml) rather than XmlSerializer? That's one of the main benefits of data contracts (same programming model for multiple projections).
(There is no way to make a parameterless constructor for an F# record.)