F#: can I extend a module with additional module by just referencing another assembly? - .net-core

I'm working on a tiny F# ADO.NET "wrapper" (yes, yet another one, besides Zaid Ajaj's Npgsql.FSharp, Pim Brouwers's Donald and many others on GitHub), and I am thinking about extending the support for different ADO.NET providers...
Basically I have a core project (ie. Michelle.Sql.Core) that contains the core types + functions, a bit similar to Dapper:
type IDbValue<'DbConnection, 'DbParameter
when 'DbConnection :> DbConnection
and 'DbParameter :> DbParameter> =
abstract ToParameter: string -> 'DbParameter
type CommandDefinition<'DbConnection, 'DbParameter, 'DbType
when 'DbConnection :> DbConnection
and 'DbParameter :> DbParameter
and 'DbType :> IDbValue<'DbConnection, 'DbParameter>> =
{ Statement: Statement
Parameters: (string * 'DbType) list
CancellationToken: CancellationToken
Timeout: TimeSpan
StoredProcedure: bool
Prepare: bool
Transaction: DbTransaction option }
First thing, you might think "Wosh there is a lot generics ornamenting your type definitions!".
Alright so first things first I'm trying to work around some limitations, most notable this one: https://github.com/fsharp/fslang-suggestions/issues/255 (along with its good friend), thought I could circumvent that issue by creating a C# project and forcing constraints in that project, it doesn't work out.
The reason I need that many generic constraints is that I want a strongly-typed connection that kinda "flow" through the calls setting the values of the different fields of that record, for example:
let playWithSQLite() =
use connection = new SQLiteConnection()
Sql.statement "INSERT INTO aTable (aColumn) VALUES(#aNumber);"
|> Sql.prepare true
|> Sql.timeout (TimeSpan.FromMinutes(1.))
|> Sql.parameters [("aNumber", SqliteDbValue.Integer 42L)]
|> Sql.executeNonQuery connection
Fyi, SqliteDbValue is defined in different assembly Michelle.Sql.Sqlite:
// https://www.sqlite.org/datatype3.html
type SqliteDbValue =
| Null
| Integer of int64
| Real of double
| Text of string
| Blob of byte array
interface IDbValue<SQLiteConnection, SQLiteParameter> with
member this.ToParameter(name) =
let parameter = SQLiteParameter()
// Not so secret impl. goes here...
parameter
The code above works, basically, the CommandDefinition record is populated via different calls defined in the core library through a Sql module (decorated with RequiredAccessAttribute).
The problem arise when the use needs to explicitly indicates the generic return type...
[<RequireQualifiedAccess>]
module Sql =
// [...]
let executeNonQuery
(connection: 'DbConnection when 'DbConnection :> DbConnection)
(commandDefinition: CommandDefinition<'DbConnection, 'DbParameter, 'DbType>
when 'DbConnection :> DbConnection
and 'DbParameter :> DbParameter
and 'DbType :> IDbValue<'DbConnection, 'DbParameter>) =
async {
// Not so secret impl. goes here
}
let executeScalar<'Scalar, .. >
(connection: 'DbConnection when 'DbConnection :> DbConnection)
(commandDefinition: CommandDefinition<'DbConnection, 'DbParameter, 'DbType>
when 'DbConnection :> DbConnection
and 'DbParameter :> DbParameter
and 'DbType :> IDbValue<'DbConnection, 'DbParameter>) =
async {
// Not so secret impl. goes here
}
So you see, in the case of the executeScalar function above, since one type has to be made explicit, it means every other generic parameter has now to be made explicit when calling that function, otherwise they are defaulted to obj, which among other things means that the end-user now needs to input 4 generic parameters:
// [...] setting up the CommandDefinition...
|> Sql.executeScalar<int64, SQLiteConnection, SQLiteParameter, SqliteDbValue> connection
and this is exactly the kind of things I would like to avoid while retaining the connection consistency.
What I tried and which is rather a clunky solution is to implement a reduced version of the executeScalar, and what I mean by:
module Michelle.Sql.Sqlite
[<RequireQualifiedAccess>]
module Sql =
let executeScalar<'Scalar> connection commandDefinition =
Sql.executeScalar<'Scalar, SQLiteConnection, SQLiteParameter, SqliteDbValue>
connection
commandDefinition
But the thing with strategy is that it essentially boils down to shadowing:
Hence this code below doesn't work:
open Michelle.Sql.Sqlite
open Michelle.Sql.Core
// [...] setting up the CommandDefinition... connection being an instance of SQLiteConnection
|> Sql.executeScalar<int64> connection
While that one does:
open Michelle.Sql.Core
open Michelle.Sql.Sqlite
// [...] setting up the CommandDefinition... connection being an instance of SQLiteConnection
|> Sql.executeScalar<int64> connection
I wish there could be a solution, I even though about static classes, but partial classes can't be defined across several assemblies.
I know that overloading is not possible with F# module functions and shadowing doesn't look like a viable solution in terms of developer experience.
So, is there any solution out there? (Putting aside creating another function with a different name or a different module with also a different name)

Koenig Lear suggested:
Not really an answer but why don't you rename your original module to CoreSQL then you can create modules for each driver type e.g. Sql.execluteScalar<'T> = CoreSql.executeScalar<'T,SQLLiteConnection, etc. and provide aliasing to every single function and never expose CoreSQL.
and this is pretty much what I ended up doing:
// open stuff goes here...
type SqliteCommandDefinition = CommandDefinition<SQLiteConnection, SQLiteParameter, SqliteDbValue>
[<RequireQualifiedAccess>]
module Sqlite =
// Other functions irrelevant to this post
let executeScalar<'Scalar> connection (commandDefinition: SqliteCommandDefinition) =
Sql.executeScalar<'Scalar, _, _, _>
connection
commandDefinition
let executeNonQuery connection (commandDefinition: SqliteCommandDefinition) =
Sql.executeNonQuery connection commandDefinition

Related

What's a good pattern to manage impossible states in Elm?

Maybe you can help. I'm an Elm beginner and I'm struggling with a rather mundane problem. I'm quite excited with Elm and I've been rather successful with smaller things, so now I tried something more complex but I just can't seem to get my head around it.
I'm trying to build something in Elm that uses a graph-like underlying data structure. I create the graph with a fluent/factory pattern like this:
sample : Result String MyThing
sample =
MyThing.empty
|> addNode 1 "bobble"
|> addNode 2 "why not"
|> addEdge 1 2 "some data here too"
When this code returns Ok MyThing, then the whole graph has been set up in a consistent manner, guaranteed, i.e. all nodes and edges have the required data and the edges for all nodes actually exist.
The actual code has more complex data associated with the nodes and edges but that doesn't matter for the question. Internally, the nodes and edges are stored in the Dict Int element.
type alias MyThing =
{ nodes : Dict Int String
, edges : Dict Int { from : Int, to : Int, label : String }
}
Now, in the users of the module, I want to access the various elements of the graph. But whenever I access one of the nodes or edges with Dict.get, I get a Maybe. That's rather inconvenient because by the virtue of my constructor code I know the indexes exist etc. I don't want to clutter upstream code with Maybe and Result when I know the indexes in an edge exist. To give an example:
getNodeTexts : Edge -> MyThing -> Maybe (String, String)
getNodeTexts edge thing =
case Dict.get edge.from thing.nodes of
Nothing ->
--Yeah, actually this can never happen...
Nothing
Just fromNode -> case Dict.get edge.to thing.nodes of
Nothing ->
--Again, this can never actually happen because the builder code prevents it.
Nothing
Just toNode ->
Just ( fromNode.label, toNode.label )
That's just a lot of boilerplate code to handle something I specifically prevented in the factory code. But what's even worse: Now the consumer needs extra boilerplate code to handle the Maybe--potentially not knowing that the Maybe will actually never be Nothing. The API is sort of lying to the consumer. Isn't that something Elm tries to avoid? Compare to the hypothetical but incorrect:
getNodeTexts : Edge -> MyThing -> (String, String)
getNodeTexts edge thing =
( Dict.get edge.from thing.nodes |> .label
, Dict.get edge.to thing.nodes |> .label
)
An alternative would be not to use Int IDs but use the actual data instead--but then updating things gets very tedious as connectors can have many edges. Managing state without the decoupling through Ints just doesn't seem like a good idea.
I feel there must be a solution to this dilemma using opaque ID types but I just don't see it. I would be very grateful for any pointers.
Note: I've also tried to use both drathier and elm-community elm-graph libraries but they don't address the specific question. They rely on Dict underneath as well, so I end up with the same Maybes.
There is no easy answer to your question. I can offer one comment and a coding suggestion.
You use the magic words "impossible state" but as OOBalance has pointed out, you can create an impossible state in your modelling. The normal meaning of "impossible state" in Elm is precisely in relation to modelling e.g. when you use two Bools to represent 3 possible states. In Elm you can use a custom type for this and not leave one combination of bools in your code.
As for your code, you can reduce its length (and perhaps complexity) with
getNodeTexts : Edge -> MyThing -> Maybe ( String, String )
getNodeTexts edge thing =
Maybe.map2 (\ n1 n2 -> ( n1.label, n2.label ))
(Dict.get edge.from thing.nodes)
(Dict.get edge.to thing.nodes)
From your description, it looks to me like those states actually aren't impossible.
Let's start with your definition of MyThing:
type alias MyThing =
{ nodes : Dict Int String
, edges : Dict Int { from : Int, to : Int, label : String }
}
This is a type alias, not a type – meaning the compiler will accept MyThing in place of {nodes : Dict Int String, edges : Dict Int {from : Int, to : Int, label : String}} and vice-versa.
So rather than construct a MyThing value safely using your factory functions, I can write:
import Dict
myThing = { nodes = Dict.empty, edges = Dict.fromList [(0, {from = 0, to = 1, label = "Edge 0"})] }
… and then pass myThing to any of your functions expecting MyThing, even though the nodes connected by Edge 0 aren't contained in myThing.nodes.
You can fix this by changing MyThing to be a custom type:
type MyThing
= MyThing { nodes : Dict Int String
, edges : Dict Int { from : Int, to : Int, label : String }
}
… and exposing it using exposing (MyThing) rather than exposing (MyThing(..)). That way, no constructor for MyThing is exposed, and code outside of your module must use the factory functions to obtain a value.
The same applies to Edge, wich I'm assuming is defined as:
type alias Edge =
{ from : Int, to : Int, label : String }
Unless it is changed to a custom type, it is trivial to construct arbitrary Edge values:
type Edge
= Edge { from : Int, to : Int, label : String }
Then however, you will need to expose some functions to obtain Edge values to pass to functions like getNodeTexts. Let's assume I have obtained a MyThing and one of its edges:
myThing : MyThing
-- created using factory functions
edge : Edge
-- an edge of myThing
Now I create another MyThing value, and pass it to getNodeTexts along with edge:
myOtherThing : MyThing
-- a different value of type MyThing
nodeTexts = getNodeTexts edge myOtherThing
This should return Maybe.Nothing or Result.Err String, but certainly not (String, String) – the edge does not belong to myOtherThing, so there is no guarantee its nodes are contained in it.

JScript.NET: Enumerating WMI collections

In JScript.NET the following snippet:
wmi.js
------
var wmi = GetObject("winmgmts:{impersonationLevel=impersonate}!\\\\.\\root\\cimv2"),
col =null, prc=null;
col=wmi.ExecQuery("SELECT * From Win32_Process", "WQL", 32);
//col=wmi.InstancesOf("Win32_Process");
var e = new Enumerator(col);
for (; !e.atEnd(); e.moveNext()){
prc = e.item();
print(prc.CommandLine);
}
compiles with:
%windir%\Microsoft.NET\Framework64\v4.0.30319\jsc.exe /platform:x64 wmi.js
and executes, but changing the WMI call with:
col=wmi.ExecQuery("SELECT * From Win32_Process", "WQL", 32);
compilation still works, while the execution gives the:
Unhandled Exception: System.InvalidCastException:
Unable to cast COM object of type 'System.__ComObject' to interface type 'System.Collections.IEnumerable'.
This operation failed because the QueryInterface call on the COM component for the interface with IID '{496B0ABE-CDEE-11D3-88E8-00902754C43A}' failed due to the following error:
'No such interface supported (Exception from HRESULT: 0x80004002
I don't understand why, since for both
InstancesOf
and
ExecQuery documentation says:
If successful, the method returns an SWbemObjectSet
Also, WSH JScript can enumerate both InstancesOf collection and ExecQuery.
First things first, remove the flag for wbemFlagForwardOnly and the ExecQuery returns an object that works as expected.
var wmi = GetObject("winmgmts:{impersonationLevel=impersonate}!\\\\.\\root\\cimv2")
, col =null, prc=null;
col=wmi.ExecQuery("SELECT * From Win32_Process");
//col=wmi.InstancesOf("Win32_Process");
var e = new Enumerator(col);
for (; !e.atEnd(); e.moveNext()){
prc = e.item();
print(prc.CommandLine);
}
For the explanation, here's a shot in the dark (I don't work with Jscript.NET every day nor am I an expert).
from https://msdn.microsoft.com/en-us/library/ms974547.aspx:
"A forward-only enumerator performs much faster than the default enumerator, because WMI doesn't maintain references to objects in the SWbemObjectSet"
from the error:
"Unable to cast COM object of type 'System.__ComObject' to interface type 'System.Collections.IEnumerable."
It seems that converting collection to enumerator requires a reference to the object being casted. With wbemFlagForwardOnly flag, there is no reference passed so cast fails.
That is how I read this. Take it for what it's worth.
An interesting thing I found when researching: there is no error with this enumerator using wscript/cscript versus executing exe from jsc/csc.
Also, it seems VBScript has no problem enumerating with these flags; check out the examples and compare - https://msdn.microsoft.com/en-us/library/ms525775(v=vs.90).aspx.

Golang basics struct and new() keyword

I was learning golang, and as I was going through the chapter that describes Structures, I came across different ways to initialize structures.
p1 := passport{}
var p2 passport
p3 := passport{
Photo: make([]byte, 0, 0),
Name: "Scott",
Surname: "Adam",
DateOfBirth: "Some time",
}
fmt.Printf("%s\n%s\n%s\n", p1, p2, p3)
While these print the values of the structures as
{ }
{ }
{ Scott Adam Some time}
, the following code below prints with an ampersand because it is a reference.
pointerp1 := &p3
fmt.Printf("%s", pointerp1)
pointerp2 := new(passport)
pointerp2.Name = "Anotherscott"
fmt.Printf("%s", pointerp2)
&{ Scott Adam Some time}&{ Anotherscott }
Kindly help me with my doubts.
in the usage pointerp1 := &p3, pointerp1 is the reference variable to p3, which holds the actual data. Similarly, what would be the actual variable that holds the data for pointerp2?
What would be the best scenarios to use these different types of initialization?
new allocates zeroed storage for a new item or type whatever and then returns a pointer to it. I don't think it really matters on if you use new vs short variable declaration := type{} it's mostly just preference
As for pointer2, the pointer2 variable holds its own data, when you do
// initializing a zeroed 'passport in memory'
pointerp2 := new(passport)
// setting the field Name to whatever
pointerp2.Name = "Anotherscott"
new allocates zeroed storage in memory and returns a pointer to it, so in short, new will return a pointer to whatever you're making that is why pointerp2 returns &{ Anotherscott }
You mainly want to use pointers when you're passing a variable around that you need to modify (but be careful of data races use mutexes or channels If you need to read and write to a variable from different functions)
A common method people use instead of new is just short dec a pointer type:
blah := &passport{}
blah is now a pointer to type passport
You can see in this playground:
http://play.golang.org/p/9OuM2Kqncq
When passing a pointer, you can modify the original value. When passing a non pointer you can't modify it. That is because in go variables are passed as a copy. So in the iDontTakeAPointer function it is receiving a copy of the tester struct then modifying the name field and then returning, which does nothing for us as it is modifying the copy and not the original.
There is variable that holds the data yet. You can dereference the pointer using *pointerp2, and even assign it that to a variable (p2 := pointerp2), but this variable would be a copy of the data. That is, modifying one no longer affects the other (http://play.golang.org/p/9yRYbyvG8q).
new tends to be less popular, especially with regard to structs. A good discussion of its purpose (hint: it came first) and use cases can be found at https://softwareengineering.stackexchange.com/a/216582.
Edit: Also, p1 is not really a different kind of initialization from p3, but instead of assigning a value to any of the type's fields they are initialized to their zero value ("" for string, nil for []byte). The same would happen for any omitted fields:
p4 := passport{
Name: "Scott",
Surname: "Adam",
}
In this case, p4.Photo and p4.DateOfBirth would still be zero-valued (nil and "" respectively). The passport{} case it just one where all the fields are omitted.
All the new keyword does is basically create a instance of the type you want. However instead of returning the plain declaration of the type, it references it and return the acutal memory address of that type in the program process heap.
I was experiencing strange phenomena in golang where my pointer declared as myptr:= new(ptrtype)
was resulting in false from if myptr==nil, so I went ahead and tried defining it as myptr:=&ptrtype{} and still didn't work. So then I just defined the pointer with new() and then i set it = nill and now it works. don't know why I didn't have to do that with the other ones.

Passing strings as task creation discriminants in Ada

I'm moving my first steps with Ada, and I'm finding that I struggle to understand how to do common, even banal, operations that in other languages would be immediate.
In this case, I defined the following task type (and access type so I can create new instances):
task type Passenger(
Name : String_Ref;
Workplace_Station : String_Ref;
Home_Station : String_Ref
);
type Passenger_Ref is access all Passenger;
As you can see, it's a simple task that has 3 discriminants that can be passed to it when creating an instance. String_Ref is defined as:
type String_Ref is access all String;
and I use it because apparently you cannot use "normal" types as task discriminants, only references or primitive types.
So I want to create an instance of such a task, but whatever I do, I get an error. I cannot pass the strings directly by simply doing:
Passenger1 := new Passenger(Name => "foo", Workplace_Station => "man", Home_Station => "bar");
Because those are strings and not references to strings, fair enough.
So I tried:
task body Some_Task_That_Tries_To_Use_Passenger is
Passenger1 : Passenger_Ref;
Name1 : aliased String := "Foo";
Home1 : aliased String := "Man";
Work1 : aliased String := "Bar";
begin
Passenger1 := new Passenger(Name => Name1'Access, Workplace_Station => Work1'Access, Home_Station => Home1'Access);
But this doesn't work either, as, from what I understand, the Home1/Name1/Work1 variables are local to task Some_Task_That_Tries_To_Use_Passenger and so cannot be used by Passenger's "constructor".
I don't understand how I have to do it to be honest. I've used several programming languages in the past, but I never had so much trouble passing a simple String to a constructor, I feel like a total idiot but I don't understand why such a common operation would be so complicated, I'm sure I'm approaching the problem incorrectly, please enlighten me and show me the proper way to do this, because I'm going crazy :D
Yes, I agree it is a serious problem with the language that discriminates of task and record types have to be discrete. Fortunately there is a simple solution for task types -- the data can be passed via an "entry" point.
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
procedure Main is
task type Task_Passenger is
entry Construct(Name, Workplace, Home : in String);
end Passenger;
task body Task_Passenger is
N, W, H : Unbounded_String;
begin
accept Construct(Name, Workplace, Home : in String) do
N := To_Unbounded_String(Name);
W := To_Unbounded_String(Workplace);
H := To_Unbounded_String(Home);
end Construct;
--...
end Passenger;
Passenger : Task_Passenger;
begin
Passenger.Construct("Any", "length", "strings!");
--...
end Main;
Ada doesn't really have constructors. In other languages, a constructor is, in essence, a method that takes parameters and has a body that does stuff with those parameters. Trying to get discriminants to serve as a constructor doesn't work well, since there's no subprogram body to do anything with the discriminants. Maybe it looks like it should, because the syntax involves a type followed by a list of discriminant values in parentheses and separated by commas. But that's a superficial similarity. The purpose of discriminants isn't to emulate constructors.
For a "normal" record type, the best substitute for a constructor is a function that returns an object of the type. (Think of this as similar to using a static "factory method" instead of a constructor in a language like Java.) The function can take String parameters or parameters of any other type.
For a task type, it's a little trickier, but you can write a function that returns an access to a task.
type Passenger_Acc is access all Passenger;
function Make_Passenger (Name : String;
Workplace_Station : String;
Home_Station : String) return Passenger_Acc;
To implement it, you'll need to define an entry in the Passenger task (see Roger Wilco's answer), and then you can use it in the body:
function Make_Passenger (Name : String;
Workplace_Station : String;
Home_Station : String) return Passenger_Acc is
Result : Passenger_Acc;
begin
Result := new Passenger;
Result.Construct (Name, Workplace_Station, Home_Station);
return Result;
end Make_Passenger;
(You have to do this by returning a task access. I don't think you can get the function to return a task itself, because you'd have to use an extended return to set up the task object and the task object isn't activated until after the function returns and thus can't accept an entry.)
You say
"I don't understand how I have to do it to be honest. I've used several programming languages in the past, but I never had so much trouble passing a simple String to a constructor, I feel like a total idiot but I don't understand why such a common operation would be so complicated, I'm sure I'm approaching the problem incorrectly, please enlighten me and show me the proper way to do this, because I'm going crazy :D"
Ada's access types are often a source of confusion. The main issue is that Ada doesn't have automatic garbage collection, and wants to ensure you can't suffer from the problem of returning pointers to local variables. The combination of these two results in a curious set of rules that force you to design your solution carefully.
If you are sure your code is good, then you can always used 'Unrestricted_Access on an aliased String. This puts all the responsibility on you to ensure the accessed variable won't disappear from underneath the task though.
It doesn't have to be all that complicated. You can use an anonymous access type and allocate the strings on demand, but please consider if you really want the strings to be discriminants.
Here is a complete, working example:
with Ada.Text_IO;
procedure String_Discriminants is
task type Demo (Name : not null access String);
task body Demo is
begin
Ada.Text_IO.Put_Line ("Demo task named """ & Name.all & """.");
exception
when others =>
Ada.Text_IO.Put_Line ("Demo task terminated by an exception.");
end Demo;
Run_Demo : Demo (new String'("example 1"));
Second_Demo : Demo (new String'("example 2"));
begin
null;
end String_Discriminants;
Another option is to declare the strings as aliased constants in a library level package, but then you are quite close to just having an enumerated discriminant, and should consider that option carefully before discarding it.
I think another solution would be the following:
task body Some_Task_That_Tries_To_Use_Passenger is
Name1 : aliased String := "Foo";
Home1 : aliased String := "Man";
Work1 : aliased String := "Bar";
Passenger1 : aliased Passenger(
Name => Name1'Access,
Workplace_Station => Work1'Access,
Home_Station => Home1'Access
);
begin
--...

F# - The type was expected to have type Async<'a> but has string -> Asnyc<'a> instead

After shamelessly pilfering a code snippet from Tomas Petricek's Blog:
http://tomasp.net/blog/csharp-fsharp-async-intro.aspx
Specifically, this one (and making a few alterations to it):
let downloadPage(url:string) (postData:string) = async {
let request = HttpWebRequest.Create(url)
// Asynchronously get response and dispose it when we're done
use! response = request.AsyncGetResponse()
use stream = response.GetResponseStream()
let temp = new MemoryStream()
let buffer = Array.zeroCreate 4096
// Loop that downloads page into a buffer (could use 'while'
// but recursion is more typical for functional language)
let rec download() = async {
let! count = stream.AsyncRead(buffer, 0, buffer.Length)
do! temp.AsyncWrite(buffer, 0, count)
if count > 0 then return! download() }
// Start the download asynchronously and handle results
do! download()
temp.Seek(0L, SeekOrigin.Begin) |> ignore
let html = (new StreamReader(temp)).ReadToEnd()
return html };;
I tried to do the following with it, and got the error on the last line:
The type was expected to have type Async<'a> but has string -> Asnyc<'a> instead
I googled the error but couldn't find anything that revealed my particular issue.
let postData = "userid=" + userId + "&password=" + password + "&source=" + sourceId + "&version=" + version
let url = postUrlBase + "100/LogIn?" + postData
Async.RunSynchronously (downloadPage(url, postData));;
Also, how would I modify the code so that it downloads a non-ending byte stream (but with occasional pauses between each burst of bytes) asynchronously instead of a string? How would I integrate reading this byte stream as it comes through? I realize this is more than one question, but since they are are all closely related I figured one question would save some time.
Thanks in advance,
Bob
P.S. As I am still new to F# please feel free to make any alterations/suggestions to my code which shows how its done in a more functional style. I'm really trying to get out of my C# mindset, so I appreciate any pointers anyone may wish to share.
Edit: I accidentally pasted in the wrong snippet I was using. I did make an alteration to Tomas' snippet and forgot about it.
When I attempt to run your code downloadPage(url, postData) doesn't work as downloadPage expects two seperate strings. downloadPage url postData is what is expected.
If you changed the let binding to tuple form, or let downloadPage(url:string, postData:string) your call would have worked as well.
To explain why you got the error you got is more complicated. Curried form creates a function that returns a function or string -> string -> Async<string> in your case. The compiler therefore saw you passing a single parameter (tuples are single items after all) and saw that the result would have to be a string -> Async<string> which is not compatible with Async<string>. Another error it could have found (and did in my case) is that string * string is not compatible with string. The exact error being Expected string but found 'a * 'b.
This is what I had:
Async.RunSynchronously (downloadPage(url, postData));;
this is what worked after continued random guessing:
Async.RunSynchronously (downloadPage url postData);;
Although, I'm not sure why this change fixed the problem. Thoughts?

Resources