Avrohom Yisroel
Avrohom Yisroel

Reputation: 9440

How do I bind together multiple monads?

I'm using the TryAsync monad in LanguageExt, but am having difficulties trying to bind multiple ones together. I'm still learning functional programming, and so could be doing this completely wrongly. Please feel free to comment on any part of my approach here.

Suppose I have the following methods that call the Google Drive API...

TryAsync<File> GetFolder(string folderId)
TryAsync<string> CreateFolder(string folderName, string parentFolderId)
TryAsync<string> UploadFile(Stream file, string fileName, string mimeType, string folderId)

...where File is a Google type for files/folders in a Google Drive.

I can call each of these individually, no problem, and can use Match to handle the result.

However, I sometimes want to call more than one, say to get the File object for a specific folder, then create a new subfolder and upload a file to it. I know I can do this as follows (air code, so please ignore any typos)...

(await GetFolder("123"))
  .Match(async folder => {
    (await CreateFolder("New folder", folder.Id))
      .Match(async newFolder => {
        (await UploadFile(stream, "New file name.txt", "text/text", newFolder.Id))
          .Match(fileId => /* do whatever with the uploaded file's Id */, ex => /* report exception */);
      }, ex => /* report exception */);
  }, ex => /* report exception */);

As you can see, this is very painful. I am sure that you are supposed to be able to chain monads together, I think using Bind, so you end up with something more like this (again, air code)...

(await GetFolder("123"))
  .Bind(folder => CreateFolder("New folder", folder.Id))
  .Bind(newFolder => UploadFile(stream, "New file name.txt", "text/text", newFolder.Id))
  .Match(fileId => /* do whatever with the uploaded file's Id */, ex => /* report exception */);

However, I can't get any code to compile like this.

One problem is that I'm not sure if my methods have the right signature. Should they return Task<T>, and have the calling code use TryAsync<T>, or am I right in having the methods themselves return TryAsync<T>?

Anyone able to advise how I should do this? Thanks

Upvotes: 1

Views: 872

Answers (1)

Mark Seemann
Mark Seemann

Reputation: 233150

Your last attempt is really close, and if you'd examined the compiler error, you probably would have figured it out:

'string' does not contain a definition for 'Id' and no accessible extension method 'Id'
accepting a first argument of type 'string' could be found (are you missing a using directive
or an assembly reference?)

The problem is newFolder.Id. The reason is that newFolder (as the error implies) is a string, not a File. This is because the result of the previous action CreateFolder returns a TryAsync<string> - not a TryAsync<File>.

Just remove the .Id property:

var actual = await GetFolder("123")
    .Bind(folder => CreateFolder("New folder", folder.Id))
    .Bind(newFolder => UploadFile(stream, "New file name.txt", "text/text", newFolder))
    .Match(fileId => handleFileId(fileId), ex => handleException(ex));

This compiles and gets the job done.

You can also write the expression in query syntax if you prefer that:

var actual = await (
    from folder in GetFolder("123")
    from newFolder in CreateFolder("New folder", folder.Id)
    from fileId in UploadFile(stream, "New file name.txt", "text/text", newFolder)
    select fileId)
    .Match(fileId => handleFileId(fileId), ex => handleException(ex));

The result is the same - the C# compiler will translate the query syntax into the previous method-binding style using Bind. Thus, the difference is only one of readability.

Sometimes query syntax is more readable, and sometimes method-call syntax is more readable. In this case, I consider the former the most readable.

Upvotes: 2

Related Questions