September 10, 2024

Map and filter - round 2

The challenge here is to write actual map and filter functions with a Haskell like syntax. The crux of the issue was learning how to accept and use a lambda function in a method I write.
This ends up being short and nice:
using System;
using System.Linq;

using Example;

namespace Example
{
    public class Functional
    {
        public static void IEshow ( IEnumerable<int> v )
        {
            Console.Write ( "Values:" );
            foreach ( int x in v ) {
                Console.Write ( $" {x}" );
            }
            Console.WriteLine ( "" );
        }

        public static IEnumerable<int> map ( Func<int, int> lfunc, int [] values )
        {
            return values.Select ( lfunc );
        }

        public static IEnumerable<int> filter ( Func<int, bool> lfunc, int [] values )
        {
            return values.Where ( lfunc );
        }
    }
}

namespace HogHeaven
{
    public class Program
    {
        public static void Main(string[] args)
        {
            int [] m_values = [7,2,3,4,5,6];
            int [] f_values = [1,2,3,4,5,6,7,8,9];
            IEnumerable<int> xx;

            xx = Functional.map ( x => x*x, m_values );
            Functional.IEshow ( xx );
            xx = Functional.filter ( x => (x % 2) == 0, f_values );
            Functional.IEshow ( xx );
        }
    }
}
// THE END
The crux of the issue here is the type designation for the first argument of the map and filter functions. The odd business of "Func<int, bool>" is what C# calls a delegate type, and it seems to have a whole raft of them. The first thing in this generic (here "int") is the type of the argument, the last thing (here "bool") is the type of the return value. Once you understand that this is the game to play, the rest is easy.

I should say something about the IEnumerable type. Here we have another generic, but never mind the generic business for now. This type is forced upon us by LINQ. We give LINQ an array of integers and it will return one of these. It works the same as an array of integers, or some other list (i.e. "collection"), but is a pain in the neck to type out.

These "map" and "filter" are just wrappers on LINQ "Select" and "Where" and the idiomatic C# programmer would want to use the latter. Making this connection is useful to me, coming from a Haskell background -- it helps me to understand one aspect of LINQ. An additional by-product of all this has been learning about lambda expressions, and in particular (in my case) how to use them as arguments to my own functions. Apparently "lambda expression" is the proper language, though I probably call them lambda functions just as often, perhaps improperly.

Just for the record, the calls in Main() can be condensed (once we rename IEshow() to show() ) down to:

show ( map ( x => x*x, [7,2,3,4,5,6] ) );
show ( filter ( x => (x % 2) == 0, [1,2,3,4,5,6,7,8,9] ) );

Combinations

LINQ allows us to Select and Where at the same time (like most database query language do). So we can construct a statement like:
	IEnumerable temps =
            from tt in temp_data where tt.year == year select tt;
Here the select does no mapping, it just says, "give me the whole record" - but it could perform a mapping function or it could select fields from the data or whatever.

In Haskell, to combine map and filter, we would have to chain the two functions together. Or we could get fancy and do "function composition", creating a new function that would do both the filtering and mapping. Operating on functions is what functional languages are all about after all. Functions that operate on functions are "higher order functions" in the functional world -- but now we are drifting away from C#, or maybe we are ....

This delegate thing

Good old C allowed you to pass a function pointer around. But this was C and all that was getting passed was the hardware address of the function. It was up to you (and maybe with some help from the compiler) to keep straight what the arguments and return type were. And being C, you were free to force it to do any crazy thing -- a feature I enjoyed, exploited, and appreciated. But in the C# world, when you start passing methods around, you have to give them a proper type, and the type indicates all the types for the method both coming and going, hence Func<int, bool> in one of our cases. These are the "delegate" types (at least that is what I am calling them at this point in time). There are many other such types and they don't all begin with "Func" so there is more to be learned.


Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org