DIVEX is a Visual Studio extension that allows C# developers to manipulate and compose functions in ways that are not available in raw C#, helping developers create more maintainable programs.
There are many types of functions that DIVEX can deal with. One type is a static method. Consider this example:
public static void Main()
{
var toThePowerThree = Math.Pow.Apply(y: 3);
var twoToThePowerThree = toThePowerThree.Invoke(2);
var fourToThePowerThree = toThePowerThree.Invoke(4);
Console.WriteLine(twoToThePowerThree);
Console.WriteLine(fourToThePowerThree);
}
Math.Pow is a static method. It has two parameters: x and y. It calculates x to the power y. In the above code, I am able to treat it as a function. I am using the Apply DIVEX operator to partially apply the Math.Pow function to fix the value of the y parameter. The result of the Apply operator is a function that takes a single parameter x and returns x to the power 3.
I am then using that function (toThePowerThree) to calculate 2 to the power 3 and 4 to the power 3.
One thing to note about the Apply operator (and DIVEX operators in general) is that it preserves parameter names. This allows me to change the two invocations of the toThePowerThree function to the following:
var twoToThePowerThree = toThePowerThree.Invoke(x: 2);
var fourToThePowerThree = toThePowerThree.Invoke(x: 4);
I was able to use the name of the parameter x.
Partial application is a powerful tool that does not exist natively in C#.
Another type of function that DIVEX can deal with is a constructor; not a static constructor, but an instance constructor. Consider this example:
public class FileDownloader
{
private readonly string serverUrl;
private readonly ILogger logger;
public FileDownloader(string serverUrl, ILogger logger)
{
this.serverUrl = serverUrl;
this.logger = logger;
}
public void Download(string filename)
{
var fullFileUrl = serverUrl + "/" + filename;
logger.Log("Downloading file using url: " + fullFileUrl);
//...
}
}
The FileDownloader class provides a Download method that allows callers to download files from some server. It has a constructor that takes two parameters. One parameter is the URL of the server to download files from, and the other parameter is a logger.
We can think of this constructor as a function that takes these two parameters and returns FileDownloader:
(string serverUrl, ILogger logger) => FileDownloader
DIVEX allows us to treat constructors as functions like this:
var create = CtorOf<FileDownloader>();
In Visual Studio with DIVEX installed, if you hover over the create variable, the following shows:
indicating that this is a function as far as DIVEX is concerned.
Now, we can use the Apply operator like this:
var create2 = create.Apply(serverUrl: "https://someserver.lab");
Here is what Visual Studio shows for create2:
Now, if we have the following logger class:
public class FileLogger : ILogger
{
private readonly string filePath;
public FileLogger(string filePath)
{
this.filePath = filePath;
}
public void Log(string message)
{
File.AppendAllText(filePath, message + Environment.NewLine);
}
}
we can use it as a value for the logger parameter of the create2 function. One way to do that is via the Apply operator like this:
var create3 = create2.Apply(logger: new FileLogger(filePath: "c:\\logfile.txt"));
Now, the create3 function has the following signature:
which means that it takes no parameters and returns FileDownloader. We can invoke it like this:
var fileDownloader = create3.Invoke();
and get an instance of FileDownloader.
That, however, is not very interesting. And it is not the way DIVEX is supposed to be used. Consider this alternative code:
var create4 = CtorOf<FileDownloader>()
.Replace(logger: CtorOf<FileLogger>());
var fileDownloader2 = create4.Invoke(
serverUrl: "https://someserver.lab",
filePath: "c:\\logfile.txt");
In this code, the Replace DIVEX operator is used. Consider this signature of create4:
It is a function that takes serverUrl and filePath parameters, and returns FileDownloader.
create4 is the result of injecting FileLogger into FileDownloader. We were able to tell FileDownloader to use FileLogger as the logger without either:
- Specifying a value for the serverUrl parameter in FileDownloader’s constructor.
- Specifying a value for the filePath parameter in FileLogger’s constructor.
In the above code, specifying these was done by invoking the create4 function.
One thing DIVEX is about, is delaying the passing of parameters to a more convenient time.
Let’s talk a bit about what exactly does Replace do. Replace allows you to replace a parameter of some function (the logger parameter in the example above) with the input parameters of another function. This can happen as long as the second function’s return value is assignable to the parameter we are replacing.
In the example above, the second function is CtorOf<FileLogger>() and it has a parameter called filePath and it returns FileLogger. FileLogger is assignable to ILogger, so the condition is met. Now, the logger parameter is replaced with the filePath parameter.
Here is the signature of the first function (the constructor of FileDownloader):
And here is the signature of the second function (the constructor of FileLogger):
And again, here is the signature of the resulting function:
At runtime, when create4 is called, it will first invoke the second function (the constructor of FileLogger) and then pass its return value as the logger parameter of the first function (the constructor of FileDownloader).
It is worth to note the following about DIVEX operators:
- DIVEX operators do not change the functions they operate on, instead they create new functions.
- DIVEX operators create functions, they don’t execute them. Only when the resulting functions are invoked, do the original functions get invoked.
Before I show you a larger example that better demonstrates the value of DIVEX, I want to talk about the Rename operator. Rename simply allows you to rename a parameter. Here is an example:
var create5 = create4.Rename(filePath_loggingFilePath: 0);
In this example, we rename the filePath parameter to loggingFilePath. Here is how the create5 function’s signature looks like:
The zero value is not relevant. It is there just to satisfy the syntax of C#.
A larger example
Now, consider this larger example of a usage of DIVEX (you can find the code in the DocumentIndexer2 project here):
var create =
CtorOf<DocumentGrabberAndProcessor>()
.Replace(documentsSource: CtorOf<FileSystemDocumentsSource>()
.Rename(path_documentsSourcePath :0))
.Replace(documentProcessor: CtorOf<IndexProcessor>())
.Replace(wordsExtractor: CtorOf<SimpleWordsExtractor>())
.Replace(documentWithExtractedWordsStore:
CtorOf<DocumentWithExtractedWordsStore>());
The composition code above involves multiple classes.
These classes will grab documents from the file system, index them, and then store the documents with index information into the database.
The DocumentGrabberAndProcessor class has two dependencies: documentsSource and documentProcessor. The Replace operator was used two times to satisfy these two dependencies. For the documentsSource dependency, FileSystemDocumentsSource is used. And for the documentProcessor dependency, IndexProcessor is used.
Notice how the path parameter of FileSystemDocumentsSource’s constructor is renamed to documentsSourcePath.
IndexProcessor has two dependencies: wordsExtractor to extract words from a document, and documentWithExtractedWordsStore to store documents and their indexing information to some store.
These dependencies are satisfied by using the Replace operator two more times.
The signature of the create function looks like this:
The first parameter of create is documentsSourcePath. It is the renamed path parameter of FileSystemDocumentsSource. Although “path” might be understandable enough in the context of the FileSystemDocumentsSource class, it is not understandable in the context of the create function. The rename was done so that the meaning of the parameter is understandable.
The second parameter of create is dataContextFactory. This parameter is originally a parameter of DocumentWithExtractedWordsStore’s constructor. Basically, this dependency is a way to specify how to create the Entity Framework DbContext object.
DIVEX can help you know the destination (and source) of a parameter. Consider the following figure:
If you hover over the create function, the signature of the function appears. If you click on some parameter, a context menu appears. One of the options is “Track destination”. If you click on that, DIVEX will show you the destination of the parameter. In the “Parameter destination tracking” window in the bottom, DIVEX says that the destination of this parameter of the create function is the constructor of DocumentWithExtractedWordsStore. If you double click on that item, CtorOf<DocumentWithExtractedWordsStore>() will be highlighted as in the figure above (in the code at line 28).
Because we didn’t use Apply or Replace to specify values for documentsSourcePath and dataContextFactory, the create function has them.
var runnable =
create.Invoke(
documentsSourcePath: settings.FolderPath,
dataContextFactory:
new DataContextFactory(settings.ConnectionString));
In the code above, we use the create function to create an instance of the composed graph by passing values for the documentsSourcePath and dataContextFactory parameters. We can invoke the create function as many times as we want passing different values for these parameters.
When composing classes using DIVEX, we can think of DIVEX as a tool for creating factories. We can specify the parameters that we want early on and leave some parameters to become parameters of the factories.
In the above example, we specified some parameters like documentsSource and documentProcessor, but we did not specify values for the documentsSourcePath and dataContextFactory parameters.
But why would we want to delay the specification of some parameters?
One reason is to delay the injection of impure classes. For more information about pure code, see my article here. Practically, it is useful to delay the injection of classes that talk directly to the file system, use the system timer, speak to the database, etc. These are usually the classes that you want to fake in your unit tests. In unit tests, you want the system timer, for example, to return a known value so that you can predict the results of your tests. In the example I used, the dataContextFactory parameter can be used to specify a fake alternative to DataContextFactory (a fake implementation of IDataContextFactory) that uses an in-memory database.
The second reason is code reuse. In your programs, you might want to create an object graph to process documents in C:\Documents and another object graph to process documents in D:\Documents. You might also want the second object graph to write the results to some network filesystem instead of the database. You can leave the parameters that affect these differences unspecified (like the documentsSourcePath parameter), and then use the resulting function multiple times to create multiple object graphs. These object graphs can themselves be composed as sub-object graphs in another bigger object graph as in the following example (from the DocumentIndexer3 project):
static void Main(string[] args)
{
var settings = ReadSettingsFromConfigurationFile();
var createDocumentProcessor = CreateDocumentGrabberAndProcessor();
var createProcessor1 =
createDocumentProcessor
.Replace(wordsExtractor: CtorOf<SimpleWordsExtractor>())
.Replace(documentWithExtractedWordsStore:
CtorOf<DocumentWithExtractedWordsStore>());
var createProcessor2 =
createDocumentProcessor
.Replace(wordsExtractor: CtorOf<RestBasedWordsExtractor>()
.Rename(url_extractorServiceUrl :0))
.Replace(documentWithExtractedWordsStore:
CtorOf<FileSystemBasedDocumentWithExtractedWordsStore>());
var createCompositeProcessor = CtorOf<CompositeRunnable>()
.ReplaceOne(runnables: createProcessor1)
.ReplaceLast(runnables: createProcessor2);
var createCompositeProcessorJoined = createCompositeProcessor
.JoinAllInputs();
var runnable =
createCompositeProcessorJoined.Invoke(
documentsSourcePath: settings.FolderPath,
dataContextFactory:
new DataContextFactory(settings.ConnectionString),
extractorServiceUrl: new Uri("http://localhost"),
outputFolderPath: settings.OutputFolderPath);
runnable.Run();
Console.WriteLine("Done. Press any key to exit");
Console.ReadKey();
}
private static VarReturn.VR1 CreateDocumentGrabberAndProcessor()
{
return CtorOf<DocumentGrabberAndProcessor>()
.Replace(documentsSource: CtorOf<FileSystemDocumentsSource>()
.Rename(path_documentsSourcePath :0))
.Replace(documentProcessor: CtorOf<IndexProcessor>());
}
Note the following about the above code:
- The code to compose the DocumentGrabberAndProcessor, FileSystemDocumentsSource, and IndexProcessor classes is moved to a method called CreateDocumentGrabberAndProcessor. Basically this method returns a factory that creates a document grabber and processor that grabs from the file system and processes documents by indexing them. Here is the signature of the function returned by CreateDocumentGrabberAndProcessor:
- The return type of CreateDocumentGrabberAndProcessor is VarReturn.VR1. However, if you see the code inside Visual Studio, it looks like this:
VarReturn.VR1 is a generated type. When I wrote the return type of CreateDocumentGrabberAndProcessor, I simply typed “varreturn” and IntelliSense converted what I wrote to VarReturn.VR1 and DIVEX generated some code to make it work. Also, DIVEX made VarReturn.VR1 look like var as in the figure above.
C# does not support using var as a return type for methods. This is why DIVEX has this feature.
- The function created by CreateDocumentGrabberAndProcessor is used twice in the Main method: Once to create the same grabber and processor we created in the DocumentIndexer2 project, and the second time to build one that uses two different classes:
- RestBasedWordsExtractor which is an alternative for SimpleWordsExtractor. This new class consumes a web service to extract words from a document.
- FileSystemBasedDocumentWithExtractedWordsStore which is an alternative to DocumentWithExtractedWordsStore which stores documents in the file system instead of the database.
Now, the createProcessor1 and createProcessor2 functions create two different document grabbers and processors. Here are the signatures of these two functions:
Both have a parameter called documentsSourcePath which is the path of the source documents in the file system. But they both have other different parameters. For example, createProcessor2 has an extractorServiceUrl parameter which is the URL of the web service to use to extract words from the documents. It also has a parameter called outputFolderPath which is used to specify the folder in the file system to store the output of processing to.
- createProcessor1 and createProcessor2 are used as dependencies for a new class called CompositeRunnable. Here is how this class is defined:
public class CompositeRunnable : IRunnable
{
private readonly IRunnable[] runnables;
public CompositeRunnable(IRunnable[] runnables)
{
this.runnables = runnables;
}
public void Run()
{
foreach (var runnable in runnables)
runnable.Run();
}
}
The CompositeRunnable class allows us to run multiple “runnables”. It is an implementation of the Composite pattern. The DocumentGrabberAndProcessor class implements the IRunnable interface and therefore it is a “runnable”.
We basically want to use this class to run the two “document grabber and processor”s represented by createProcessor1 and createProcessor2. Because the parameter of CompositeRunnable’s constructor is an array of IRunnable, we have to use ReplaceOne and ReplaceLast instead of the Replace operator. These two operators are similar to Replace, but they work on arrays (and ImmutableArray<T>). You can think of ReplaceOne as a tool to inject a single dependency into the array. The function resulting from ReplaceOne will still have the array dependency (e.g. IRunnable[]). Consider the signature of the function returned by ReplaceOne. Here is the relevant code segment for convenience:
var createCompositeProcessor = CtorOf<CompositeRunnable>()
.ReplaceOne(runnables: createProcessor1)
.ReplaceLast(runnables: createProcessor2);
The documentsSourcePath and dataContextFactory parameters come from createProcessor1. The runnable parameter is also here. This enables you to use ReplaceOne as many times as you want to inject values into the array. Every time you use it, the injected dependency will be injected into the next element in the array. The last dependency to inject into the array must be injected by using ReplaceLast. Otherwise, the resulting function would still have the array dependency.
- The createCompositeProcessor function has two parameters called documentsSourcePath:
Both createProcessor1 and createProcessor2 have a parameter called documentsSourcePath (because each one of them will create an instance of FileSystemDocumentsSource eventually). This means that createCompositeProcessor will have two parameters with this name. A DIVEX function can have more than one parameter with the same name. That doesn’t mean that it is a good thing to have that in your functions. You should get rid of duplication in parameter names quickly.
There are two cases that we should consider:
1) The two parameters should really be one parameter because they should have the same value. In this case, we can join the parameters together into a single parameter. DIVEX provides three operators to do that: JoinByName, JoinByType, and JoinAllInputs.
JoinByName can be used to join all parameters that have a specific name that we provide when we use the operator. JoinAllInputs is like JoinByName but we don’t have to provide a name. It finds all parameters that have the same name and joins them. For example, if there are two parameters named foo and three parameters named bar, the two foo parameters will be joined together and the three bar parameters will be joined together.
JoinByType allows us to join all parameters that have a specific type that we provide when we use the operator.
In the code example, JoinAllInputs is used to “join” the two documentsSourcePath parameters. This operator joins all parameters that have the same name. This results in a new function with a single documentsSourcePath parameter. When this new function (createCompositeProcessorJoined ) is invoked, it uses the argument passed for documentsSourcePath as arguments for the two documentsSourcePath parameters of createCompositeProcessor.
2) The two parameters are intended to be two parameters because the consumer of the function (or the functions that use this function) wants to be able to vary the values of the parameters separately. In this case, the parameters should have two different names. The way to do that is to use the Rename operator to rename the individual parameters to a more specific name in an earlier stage. For example, if you want to have separate documentsSourcePath parameters for the two “document grabber and processor”s, you would do something like this (the code is in the DocumentIndexer4 project):
var createProcessor1 =
createDocumentProcessor
.Rename(documentsSourcePath_documentsSourcePathForProcessor1: 0)
.Replace(wordsExtractor: CtorOf<SimpleWordsExtractor>())
.Replace(documentWithExtractedWordsStore:
CtorOf<DocumentWithExtractedWordsStore>());
var createProcessor2 =
createDocumentProcessor
.Rename(documentsSourcePath_documentsSourcePathForProcessor2: 0)
.Replace(wordsExtractor: CtorOf<RestBasedWordsExtractor>()
.Rename(url_extractorServiceUrl :0))
.Replace(documentWithExtractedWordsStore:
CtorOf<FileSystemBasedDocumentWithExtractedWordsStore>());
var createCompositeProcessor = CtorOf<CompositeRunnable>()
.ReplaceOne(runnables: createProcessor1)
.ReplaceLast(runnables: createProcessor2);
The documentsSourcePath parameters in createProcessor1 and createProcessor2 were renamed before composing them together via CompositeRunnable. Therefore, there is no need to use a join operator in this code. Later, when we invoke the createCompositeProcessor function to create the runnable, we pass two different values for the two parameters:
var runnable =
createCompositeProcessor.Invoke(
documentsSourcePathForProcessor1:settings.FolderPath,
documentsSourcePathForProcessor2: settings.FolderPath2,
dataContextFactory:
new DataContextFactory(settings.ConnectionString),
extractorServiceUrl: new Uri("http://localhost"),
outputFolderPath: settings.OutputFolderPath);
I want to diverge a little bit to talk about performance. DIVEX works by composing functions. This means that every time we use a DIVEX operator a new function is created that calls the manipulated functions. If you use the Rename operator a hundred times for example, and then invoke the resulting function, there will be 100 intermediate method calls between the resulting function and the original function. In most cases, the cost of this is negligible. For example, imagine that the original function reads from the database. The cost of reading from the database will make the cost of having 100 extra frames in the call stack look very small. But in other cases, the added layers of method calls have a non-negligible cost. To solve this problem, DIVEX has an Optimize operator. Take a look at the DocumentIndexer5 project. Here is the relevant code:
var createCompositeProcessorOptimized = createCompositeProcessor.Optimize();
createCompositeProcessorOptimized will have the exact same behavior of createCompositeProcessor. However, createCompositeProcessorOptimized will be faster. Consider this auto-generated Invoke method of the function object returned by Optimize (formatted to make it more readable for this article):
public CompositeRunnable Invoke(
string documentsSourcePathForProcessor1,
IDataContextFactory dataContextFactory,
string documentsSourcePathForProcessor2,
Uri extractorServiceUrl,
string outputFolderPath)
{
var documentWithExtractedWordsStore =
new FileSystemBasedDocumentWithExtractedWordsStore(
outputFolderPath: outputFolderPath);
var wordsExtractor = new RestBasedWordsExtractor(
url: extractorServiceUrl);
var documentProcessor = new IndexProcessor(
wordsExtractor: wordsExtractor,
documentWithExtractedWordsStore: documentWithExtractedWordsStore);
var documentsSource = new FileSystemDocumentsSource(
path: documentsSourcePathForProcessor2);
var runnables = new DocumentGrabberAndProcessor(
documentsSource: documentsSource,
documentProcessor: documentProcessor);
var documentWithExtractedWordsStore1 =
new DocumentWithExtractedWordsStore(
dataContextFactory: dataContextFactory);
var wordsExtractor1 = new SimpleWordsExtractor();
var documentProcessor1 = new IndexProcessor(
wordsExtractor: wordsExtractor1,
documentWithExtractedWordsStore: documentWithExtractedWordsStore1);
var documentsSource1 = new FileSystemDocumentsSource(
path: documentsSourcePathForProcessor1);
var runnables1 = new DocumentGrabberAndProcessor(
documentsSource: documentsSource1,
documentProcessor: documentProcessor1);
return new CompositeRunnable(
runnables:
new IRunnable[] {runnables1}
.Concat(new IRunnable[] {runnables})
.ToArray());
}
The construction of all the classes exists in one method which is very fast. Try to look at the code generated for createCompositeProcessor (you would need to start from the generated ReplaceLastClass). It will involve multiple generated classes, not a single method.
Ok. That is all I want to say about optimization.
One thing DIVEX is about is the bubbling up of parameters. If a function (e.g. a constructor) gets a new parameter, this parameter will bubble up to the functions that are composed of this function. For example, in the DocumentIndexer6 project (which is a modified copy of the DocumentIndexer3 project), I added a parameter called fileSystem of type IFileSystem to the constructor of FileSystemDocumentsSource. The addition of this parameter makes the FileSystemDocumentsSource class more testable because all interactions between this class and the file system go through this interface. Now, as a result of this addition, the following functions have an additional parameter called fileSystem:
- The two functions returned by the two usages of the Replace operator inside the CreateDocumentGrabberAndProcessor method.
- The function returned by CreateDocumentGrabberAndProcessor.
- createDocumentProcessor
- createProcessor1
- createProcessor2
- createCompositeProcessor (two instances of this parameter)
- createCompositeProcessorJoined
This is what I mean by parameters bubbling up. And this makes your programs more maintainable. You don’t have to pass parameters manually from high-level functions to low-level functions.
In DocumentIndexer7, I made the code more testable by using IFileSystem also in the FileSystemBasedDocumentWithExtractedWordsStore class. I also created another abstraction, IRestClient, and used it in the RestBasedWordsExtractor class. This enables me to provide a fake implementation of IRestClient in a test. I also extracted all of the code that creates the createDocumentProcessorJoined function into a method called CreateApplication. Here is how the code looks like:
static void Main(string[] args)
{
var settings = ReadSettingsFromConfigurationFile();
var createDocumentProcessorJoined = CreateApplication();
var runnable =
createDocumentProcessorJoined.Invoke(
documentsSourcePath: settings.FolderPath,
dataContextFactory: new DataContextFactory(settings.ConnectionString),
extractorServiceUrl: new Uri("http://localhost"),
outputFolderPath: settings.OutputFolderPath,
fileSystem: new FileSystem(),
restClient: new RestClient());
//...
}
public static VarReturn.VR2 CreateApplication()
{
var createDocumentProcessor = CreateDocumentGrabberAndProcessor();
var createProcessor1 =
createDocumentProcessor
.Replace(wordsExtractor: CtorOf<SimpleWordsExtractor>())
.Replace(documentWithExtractedWordsStore: CtorOf<DocumentWithExtractedWordsStore>());
var createProcessor2 =
createDocumentProcessor
.Replace(wordsExtractor: CtorOf<RestBasedWordsExtractor>()
.Rename(url_extractorServiceUrl: 0))
.Replace(documentWithExtractedWordsStore:
CtorOf<FileSystemBasedDocumentWithExtractedWordsStore>());
var createCompositeProcessor = CtorOf<CompositeRunnable>()
.ReplaceOne(runnables: createProcessor1)
.ReplaceLast(runnables: createProcessor2);
return createCompositeProcessor
.JoinAllInputs();
}
createDocumentProcessorJoined has the following signature:
This function has parameters for all the things we might want to fake in a unit test!
Now, in the TestingForDocumentIndexer7 project, I am doing just that. Here is the relevant code:
//Arrange
var createSut = Program.CreateApplication();
using (var effortConnection = DbConnectionFactory.CreateTransient())
{
var mockFileSystem = new MockFileSystem();
mockFileSystem.Directory.CreateDirectory("C:\\Documents");
mockFileSystem.Directory.CreateDirectory("C:\\Output");
mockFileSystem.File.WriteAllText("C:\\Documents\\document1.txt", "car house");
var restClient = A.Fake<IRestClient>();
A.CallTo(() => restClient.Post(new Uri("https://fakeservice.com/GetWords"),
A<string>.Ignored))
.ReturnsLazily((Uri url, string body) =>
{
var fakeWords = body
.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries)
.Distinct()
.Select(x => x + "_fake")
.ToArray();
return JsonConvert.SerializeObject(fakeWords);
});
var fakeDataContextFactory = new FakeDataContextFactory(effortConnection);
var runnable = createSut.Invoke(
documentsSourcePath: "C:\\Documents",
fileSystem: mockFileSystem,
dataContextFactory: fakeDataContextFactory,
extractorServiceUrl: new Uri("https://fakeservice.com"),
restClient: restClient,
outputFolderPath: "C:\\Output");
//Act
runnable.Run();
//Assert...
In this testing code, I am calling the CreateApplication method to create a createSut function. This createSut function has the same signature as createDocumentProcessorJoined in the previous example. I am using this function here and passing fake alternatives for the fileSystem, dataContextFactory, and restClient parameters.
If one class in the code requires a new dependency, I will just add the dependency parameter to the class’s constructor, and then come to the testing code and pass the appropriate value to the createSut function invocation.
So far, I have showed you examples of usages of DIVEX that are mostly object-oriented. DIVEX has first-class support for functional programming. Actually, support for treating constructors as functions was a late addition to DIVEX. The support for functional programming was the main drive for creating DIVEX. The ability to treat static methods as functions is one feature to support functional programming.
Take a look at the DocumentIndexer8 project. I converted all classes into static methods and deleted the interfaces. For example, the RestBasedWordsExtractor class which looks like this in DocumentIndexer7:
public class RestBasedWordsExtractor : IWordsExtractor
{
private readonly Uri url;
private readonly IRestClient restClient;
public RestBasedWordsExtractor(Uri url, IRestClient restClient)
{
this.url = url;
this.restClient = restClient;
}
public string[] GetWords(string content)
{
var actionUrl = new Uri(url, "/GetWords");
return JsonConvert.DeserializeObject<string[]>(
restClient.Post(actionUrl, content));
}
}
is now a static method inside the RestModule static class:
public static string[] GetWordsUsingRestService(
Uri url,
Func<Uri /*url*/, string /*body*/, string> post,
string content)
{
var actionUrl = new Uri(url, "/GetWords");
return JsonConvert.DeserializeObject<string[]>(post(actionUrl, content));
}
Instead of using the IRestClient interface (which was deleted), a Func<Uri, string, string> is used. It is worth noting that I use comments here to help myself understand the parameters inside the Func. DIVEX understands these comments and will treat them as parameter names. This is how the GetWordsUsingRestService function looks like in IntelliSense:
Of course, we could have created a special delegate with named parameters to represent the post operation.
Now look at how we compose the functions in Program.cs. Here is the CreateApplication method (modified for discussion purposes):
public static VarReturn.VR2 CreateApplication()
{
var grabAndProcessDocuments = CreateDocumentGrabberAndProcessor();
var process1 =
grabAndProcessDocuments
.Inject(extractWords: MainModule.GetWords)
.Inject(storeDocumentWithExtractedWords: StorageModule.StoreToTheDatabase);
//The separation of the process2Step1 function into its own variable is for
//discussion purposes. It does not exist in the source code
var process2Step1 = grabAndProcessDocuments
.Inject(extractWords: RestModule.GetWordsUsingRestService)
.Rename(url_extractorServiceUrl: 0);
var process2 =
process2Step1
.Inject(storeDocumentWithExtractedWords: StorageModule.StoreToTheFileSystem);
var processMultiple = MainModule.RunMultiple
.InjectOne(runnables: process1)
.InjectLast(runnables: process2);
return processMultiple
.JoinAllInputs();
}
Notice that instead of using the Replace operator, we use the Inject operator. Inject is similar to Replace, but it works when the parameter to specify is a function (e.g. Func<T1,TResult>), not a simple value. It allows you to inject a function as a value for a function parameter (e.g. Func or Action parameter in another function), even if the injected function has more parameters. This enables the bubbling up of parameters in the injected function.
For example, the storeDocumentWithExtractedWords parameter of the process2Step1 function has the following signature:
Meaning it is a function that takes InputDocumentWithExtractedWords as input and returns nothing. However, the function injected as a value for this parameter is the StorageModule.StoreToTheFileSystem function which has the following signature:
One thing you might have noticed if you opened the DocumentIndexer8 project is the definitions for surrogate classes:
Here, three surrogate classes are defined for RestModule, MainModule, and StorageModule. C# does not support treating static methods as functions (in the way DIVEX does). In order for DIVEX to be able to treat static methods as function, it creates surrogate classes. Without the displayed adornments, here is how the above code looks like:
The StorageModule type used in the Program class, e.g. in the CreateApplication method, is not the StorageModule class defined in the Implementations namespace. It is a surrogate class. It is a type named StorageModule nested inside the Program class. When you reference StorageModule.StoreToTheFileSystem, DIVEX generates a property in the surrogate class that returns a function.
Within Visual Studio (with DIVEX installed of course), the process of creating a surrogate class can be done using IntelliSense. The following demonstrates the process: