So, as a follow up to this post I'm in the final stages of preparing a presentation for this Friday introducing an audience of (mostly) fairly experienced developers to F# and F# syntax. The main reason for this is to get a number of people up to speed enough on reading F# that they can have a better experience at the Progressive F# Tutorials at the end of the month. So the aim here isn't to get people fully autonomous and writing code right now, but to allow them to read the bulk of the example code in the tutorials and follow what's going on.
The general approach I've gone for is to set up a Git repository that has a series of tagged snap shots I can check out as I work through the concepts I'm planning to cover. This will enable me to actually demonstrate and run pieces of code, answer questions and make live modifications and then always jump back to a known starting point for the next section of the talk. Given the people involved have all done some .net development and I don't need to cover things like Visual Studio usage and projects, all of the code is contained in a single Program.fs file in a console app. I've included the snapshot of the file from each tagged commit below, with a brief overview of what I'm planning to introduce before skipping to the next snapshot.
With a full screen Visual Studio editing session, I should be able to make the code large enough to be visible and reasonably rapidly guide people to the areas where the code has changed.
A combination of the excellent PaperCut project and a local 'http request to email' service pretending to be an SMS sender, we should be able to see messages being generated by the code as we go along.
After the session, I'm planning to mention Chris Marinos' koans and Try F# (especially given that Rachel Reese is running a session at the tutorials for those who are going).
Please note that these are up here for comment and suggestions at this point - I'll be pushing up the actual Git repository and a screencast (I hope) after the event. The code is designed to be a prop for the talk rather than an independent resource - for that I'd always point people to the koans/Try F# first.
So, show me the codez:
Tag '1'
Nothing too exciting here :). This is just a place holder while giving the introduction, although I will also point out the lack of required boiler plate compared to C#.
1
// Let's send an email!
Tag '2'
Introduce the open keyword, let for value assignment, and give people a feel that they're not completely leaving their nice safe .net world behind.
123456789
// Let's send an email!openSystem.Net.Mail// We only use the 'new' keyword here because SmtpClient is disposableletsmtpClient=newSmtpClient("smtp.local")// And given it's disposable, we should really dispose of it...smtpClient.Dispose()
Tag '3'
Talk about functions, show parameter application, introduce the pipe operator.
Some discussion about type inference will probably happen here.
123456789101112131415161718192021222324
openSystem.Net.MailletsmtpClient=newSmtpClient("smtp.local")// This is a functionletsendMessageclientmessage=// We'd better do something here to actually// send a message...printfn"I haven't sent a mail message!"()sendMessagesmtpClient"My message"smtpClient.Dispose()// And we'll add this so we can see the output// before it disappearsSystem.Console.ReadLine()|>ignore// This line would be the same as writing://// ignore (System.Console.Readline())//// but you have to admit that this is a bit // more readable
Tag '4'
Introduce the use keyword, show property assignment with <-.
12345678910111213141516171819
openSystem.Net.MailletsmtpClient=newSmtpClient("smtp.local")letsendMessageclientmessage=(* Because we used 'use' this will get disposed at the end of the declaring scope *)usemailMessage=newMailMessage("[email protected]","[email protected]")(* This is have you assign a parameter *)mailMessage.Subject<-"Message subject"mailMessage.Body<-messagesmtpClient.SendmailMessageprintfn"I've sent a mail message!"sendMessagesmtpClient"My message"smtpClient.Dispose()System.Console.ReadLine()|>ignore
Tag '5'
List syntax and introduce the Seq module. Example of currying.
1234567891011121314151617181920212223242526
openSystem.Net.MailletsmtpClient=newSmtpClient("smtp.local")letsendMessageclientmessage=usemailMessage=newMailMessage("[email protected]","[email protected]")mailMessage.Subject<-"Message subject"mailMessage.Body<-messagesmtpClient.SendmailMessageprintfn"I've sent a mail message!"(* But really, what you want computers foris doing the same thing lots of times... *)letmyMessages=["My first message""My second message""My third and final message"]myMessages|>Seq.iter(sendMessagesmtpClient)(* Let's have some vindaloo with that map *)smtpClient.Dispose()System.Console.ReadLine()|>ignore
Tag '6'
An async workflow. Turns out that SmtpClient is not as clean for that as you would hope - it's async send methods don't appear to be thread safe (wait, what?) and even the relatively recent looking SendMailAsync method returns a Task rather than a Task<'T>. Having said that, it shows that even in less than ideal circumstances, you can leverage the async stuff even when interfacing with older .net code from other languages.
openSystem.Net.Mail(* But really, what you want computers foris doing the same thing lots of times... at the same time! *)letsendMessagemessage=async{// Move the client inside because...// have _you_ checked if it's thread safe?usesmtpClient=newSmtpClient("smtp.local")usemailMessage=newMailMessage("[email protected]","[email protected]")mailMessage.Subject<-"Message subject"mailMessage.Body<-messagedo!smtpClient.SendMailAsync(mailMessage)|>Async.AwaitIAsyncResult|>Async.Ignoreprintfn"I've sent a mail message!"}letmyMessages=["My first message""My second message""My third and final message"]myMessages|>Seq.mapsendMessage|>Async.Parallel|>Async.RunSynchronously|>fun_->printfn"Finished all sends!"System.Console.ReadLine()|>ignore
Tag '7'
Tuples! Showing both construction and deconstruction syntax. Also discuss that this is how we pass multiple parameters to methods on classes.
In the live coding for this one, I'll make sure to demonstrate adding and removing brackets in different places.
openSystem.Net.MailletsendMessagemessageDetails=async{letaddress,body=messageDetailsusesmtpClient=newSmtpClient("smtp.local")usemailMessage=newMailMessage("[email protected]",address)mailMessage.Subject<-"Message subject"mailMessage.Body<-bodydo!smtpClient.SendMailAsync(mailMessage)|>Async.AwaitIAsyncResult|>Async.Ignoreprintfn"I've sent a mail message!"}(* But we probably don't want to send all of theseto the same person. *)letmyMessages=["[email protected]","My first message""[email protected]","My second message""[email protected]","My third and final message"]myMessages|>Seq.mapsendMessage|>Async.Parallel|>Async.RunSynchronously|>fun_->printfn"Finished all sends!"System.Console.ReadLine()|>ignore
openSystem.Net.Mail(* How about if I want to pass lots of different bitsof information in? *)typemessageDetails={toAddress:stringfromAddress:stringbody:string}letsendMessagemessageDetails=async{usesmtpClient=newSmtpClient("smtp.local")usemailMessage=newMailMessage(messageDetails.fromAddress,messageDetails.toAddress)mailMessage.Subject<-"Message subject"mailMessage.Body<-messageDetails.bodydo!smtpClient.SendMailAsync(mailMessage)|>Async.AwaitIAsyncResult|>Async.Ignoreprintfn"I've sent a mail message!"}letmyMessages=[{toAddress="[email protected]";fromAddress="[email protected]";body="My first message"}{toAddress="[email protected]";fromAddress="[email protected]";body="My second message"}{toAddress="[email protected]";fromAddress="[email protected]";body="My third message"}]myMessages|>Seq.mapsendMessage|>Async.Parallel|>Async.RunSynchronously|>fun_->printfn"Finished all sends!"System.Console.ReadLine()|>ignore
Tag '9'
Use discriminated unions for modeling business inputs. In this case, building a MessageDetails class that can contain the details of either an email or an SMS send request.
Also has a 2nd, maybe slightly more idiomatic implementation of an async workflow.
I'm hoping to get at least this far in the session. The rest of it would be nice, but if we get here then I'll be happy I've covered at least the basics.
openSystem.IOopenSystem.NetopenSystem.Net.Mail(* But some people have given us mobilenumbers rather than email addresses *)typeEmailDetails={toAddress:stringfromAddress:stringbody:string}typeSmsDetails={toNumber:stringfromNumber:stringmessage:string}typeMessageDetails=|EmailofEmailDetails|SmsofSmsDetailsletsendEmailmessageDetails=async{usesmtpClient=newSmtpClient("smtp.local")usemailMessage=newMailMessage(messageDetails.fromAddress,messageDetails.toAddress)mailMessage.Subject<-"Message subject"mailMessage.Body<-messageDetails.bodydo!smtpClient.SendMailAsync(mailMessage)|>Async.AwaitIAsyncResult|>Async.Ignoreprintfn"I've sent a mail message!"}letsendSmsmessageDetails=async{lethttp=HttpWebRequest.Create("http://sms.local"):?>HttpWebRequesthttp.Method<-"POST"letmessagePayload=sprintf"To: %s\nFrom: %s\nMessage: %s"messageDetails.toNumbermessageDetails.fromNumbermessageDetails.messageusing(http.GetRequestStream())(funstream->usesw=newStreamWriter(stream)sw.Write(messagePayload))let!response=http.GetResponseAsync()|>Async.AwaitTaskif(response:?>HttpWebResponse).StatusCode<>HttpStatusCode.OKthenfailwith"Http request failed!"printfn"I've sent an SMS!"}letsendMessagemessage=matchmessagewith|Emaildetails->sendEmaildetails|Smsdetails->sendSmsdetailsletmyMessages=[Email{toAddress="[email protected]";fromAddress="[email protected]";body="My first message"}Email{toAddress="[email protected]";fromAddress="[email protected]";body="My second message"}Email{toAddress="[email protected]";fromAddress="[email protected]";body="My third message"}Sms{toNumber="+447777123123";fromNumber="+447888321321";message="Hello by sms"}Sms{toNumber="+447777123124";fromNumber="+447888321321";message="Hello by sms x2"}Sms{toNumber="+447777123123";fromNumber="+447888321321";message="Hello by sms x3"}]myMessages|>Seq.mapsendMessage|>Async.Parallel|>Async.RunSynchronously|>fun_->printfn"Finished all sends!"System.Console.ReadLine()|>ignore
openSystem.IOopenSystem.NetopenSystem.Net.MailtypeEmailDetails={toAddress:stringfromAddress:stringbody:string}typeSmsDetails={toNumber:stringfromNumber:stringmessage:string}typeMessageDetails=|EmailofEmailDetails|SmsofSmsDetails(* But what if some people have given us invalid data?Our SMS sender requires full numbers with nationalcodes - let's add some validation! *)let(|ValidSmsRequest|InvalidSmsRequest|)details=// Hmm. Bananas. My favourite.letregex=System.Text.RegularExpressions.Regex(@"^\+\d\d")ifregex.IsMatch(details.toNumber)&®ex.IsMatch(details.fromNumber)thenValidSmsRequestdetailselseInvalidSmsRequest"You must include the +xx country prefix on mobile numbers."letsendEmailmessageDetails=async{usesmtpClient=newSmtpClient("smtp.local")usemailMessage=newMailMessage(messageDetails.fromAddress,messageDetails.toAddress)mailMessage.Subject<-"Message subject"mailMessage.Body<-messageDetails.bodydo!smtpClient.SendMailAsync(mailMessage)|>Async.AwaitIAsyncResult|>Async.Ignoreprintfn"I've sent a mail message!"}(* We've moved the SMS post logic into this methodwithout change - no validation here.We've marked it private so no one else can call itby mistake *)letprivatepostSmsmessageDetails=async{lethttp=HttpWebRequest.Create("http://sms.local"):?>HttpWebRequesthttp.Method<-"POST"letmessagePayload=sprintf"To: %s\nFrom: %s\nMessage: %s"messageDetails.toNumbermessageDetails.fromNumbermessageDetails.messageusing(http.GetRequestStream())(funstream->usesw=newStreamWriter(stream)sw.Write(messagePayload))let!response=http.GetResponseAsync()|>Async.AwaitTaskif(response:?>HttpWebResponse).StatusCode<>HttpStatusCode.OKthenfailwith"Http request failed!"printfn"I've sent an SMS!"}(* And this is where we do our validation *)letsendSmsmessageDetails=matchmessageDetailswith|ValidSmsRequestdetails->postSmsdetails|InvalidSmsRequesterror->async{printfn"Sms sending error: %s"error}letsendMessagemessage=matchmessagewith|Emaildetails->sendEmaildetails|Smsdetails->sendSmsdetailsletmyMessages=[Email{toAddress="[email protected]";fromAddress="[email protected]";body="My first message"}Email{toAddress="[email protected]";fromAddress="[email protected]";body="My second message"}Email{toAddress="[email protected]";fromAddress="[email protected]";body="My third message"}Sms{toNumber="+447777123123";fromNumber="+447888321321";message="Hello by sms"}Sms{toNumber="+447777123124";fromNumber="+447888321321";message="Hello by sms x2"}Sms{toNumber="+447777123123";fromNumber="+447888321321";message="Hello by sms x3"}Sms{toNumber="447777123123";fromNumber="+447888321321";message="I won't be sent!"}Sms{toNumber="+447777123123";fromNumber="+ab7888321321";message="Neither will I!"}Sms{toNumber="Bob";fromNumber="+ab7888321321";message="..and I definitely won't!"}]myMessages|>Seq.mapsendMessage|>Async.Parallel|>Async.RunSynchronously|>fun_->printfn"Finished all sends!"System.Console.ReadLine()|>ignore
Tag '11'
The extra credit section! I very much doubt I'll get this far in a one hour session, but if I do this is just some fun playing with type providers. Also covers randomness such as:
generating infinite sequences using seq expressions
double back tick identifiers
pattern matching with guards
a bit more of a workout of the Seq module
we can have a lot of discussion of lazy evaluation, because this code is just insanely broken without it
openSystemopenSystem.IOopenSystem.NetopenSystem.Net.MailopenSystem.Text.RegularExpressionstypeEmailDetails={toAddress:stringfromAddress:stringbody:string}typeSmsDetails={toNumber:stringfromNumber:stringmessage:string}typeMessageDetails=|EmailofEmailDetails|SmsofSmsDetailslet(|ValidSmsRequest|InvalidSmsRequest|)details=letregex=Regex(@"^\+\d\d")ifregex.IsMatch(details.toNumber)&®ex.IsMatch(details.fromNumber)thenValidSmsRequestdetailselseInvalidSmsRequest"You must include the +xx country prefix on mobile numbers."letsendEmailmessageDetails=async{usesmtpClient=newSmtpClient("smtp.local")usemailMessage=newMailMessage(messageDetails.fromAddress,messageDetails.toAddress)mailMessage.Subject<-"Message subject"mailMessage.Body<-messageDetails.bodydo!smtpClient.SendMailAsync(mailMessage)|>Async.AwaitIAsyncResult|>Async.Ignoreprintfn"I've sent a mail message!"}letprivatepostSmsmessageDetails=async{lethttp=HttpWebRequest.Create("http://sms.local"):?>HttpWebRequesthttp.Method<-"POST"letmessagePayload=sprintf"To: %s\nFrom: %s\nMessage: %s"messageDetails.toNumbermessageDetails.fromNumbermessageDetails.messageusing(http.GetRequestStream())(funstream->usesw=newStreamWriter(stream)sw.Write(messagePayload))let!response=http.GetResponseAsync()|>Async.AwaitTaskif(response:?>HttpWebResponse).StatusCode<>HttpStatusCode.OKthenfailwith"Http request failed!"printfn"I've sent an SMS!"}letsendSmsmessageDetails=matchmessageDetailswith|ValidSmsRequestdetails->postSmsdetails|InvalidSmsRequesterror->async{printfn"Sms sending error: %s"error}letsendMessagemessage=matchmessagewith|Emaildetails->sendEmaildetails|Smsdetails->sendSmsdetails(* And now for something completely different...Let's send a bunch of actors and celebrities a selection of astronomical data. Because, you know. Why not?If you're running this code at home, you'll needto install the nuget package from the packages.configfile *)openFSharp.DataletFreebaseKey=letrecgetKey(dir:DirectoryInfo)=matchdir.EnumerateFiles("freebase.key")with|fileswhenSeq.isEmptyfiles->getKey(dir.Parent)|files->(Seq.headfiles).OpenText().ReadToEnd().Trim()letdir=DirectoryInfo(Directory.GetCurrentDirectory())getKeydirtypeFreebaseProvider=FreebaseDataProvider<Key="api key goes here">letfreebase=FreebaseProvider.GetDataContext()(* If you don't have an api key you can delete lines88 to the end of this comment, and uncomment the line below.It will limit how many times you can run the programbefore it starts throwing authentication errors,though - there's a fairly strict rate limit. *)//let freebase = FreebaseData.GetDataContext()letactors=freebase.``ArtsandEntertainment``.Film.``Filmactors``|>Seq.filter(funa->not<|Seq.isEmptya.``Filmperformances``)(* You get a (virtual) cookie if you can work out why I've added the filter below *)|>Seq.filter(funa->(a.``Filmperformances``|>Seq.head).Film.Name.[0..0]|>Regex("[a-zA-Z]").IsMatch)|>Seq.filter(funa->not<|Seq.isEmptya.``Countryofnationality``)|>Seq.take20letencode(str:string)=letclean=Regex("\W")clean.Replace(str,"-")letemailAddresses=seq{foractorinactors->letname=actor.Name|>encodeletdomain=(actor.``Filmperformances``|>Seq.head).Film.Name|>encodeletcountryCode=match(actor.``Countryofnationality``|>Seq.head).``ISOAlpha2``with|alphawhenSeq.isEmptyalpha->"com"|alphawhen(Seq.headalpha).ToLower()="us"->"com"|alpha->sprintf"co.%s"<|(Seq.headalpha).ToLower()sprintf"%s@%s.%s"namedomaincountryCode}(* We're going to need 20 planets for our 20 celebrities,so we'll repeat the planets as many times as we need *)letplanets=seq{whiletruedoyield!freebase.``ScienceandTechnology``.Astronomy.Planets}letmessages=seq{forplanetinplanets->sprintf"""Hi there!We thought you might be interested to know that:The planet %s has:%d moons!An average orbital velocity of %Am/s!And is also known as:%sRegards,Astro """planet.Name(planet.``Orbitedby``|>Seq.length)planet.``AverageOrbitalSpeed``(planet.``Alsoknownas``|>String.concat", ")}letcombineAddressAndMessage(address,message)=Email{toAddress=addressfromAddress="[email protected]"body=message}letmyMessages=Seq.zipemailAddressesmessages|>Seq.mapcombineAddressAndMessagemyMessages|>Seq.mapsendMessage|>Async.Parallel|>Async.RunSynchronously|>fun_->printfn"Finished all sends!"System.Console.ReadLine()|>ignore