4
\$\begingroup\$

I am porting C# code to F# that makes use of LINQ's Join() extension method. Just as I use that in method call chains, I would of course like to have an F# function to pipe into. However, there is no equivalent in the Seq module, and while the same result could also be achieved with a nested lambda using Seq.where, I'd also want something that tells me by name that it is in fact a join.

I came up with this function using a simple query expression:

let seqJoin innerKeySelector outerKeySelector resultSelector (innerSequence : 'b seq) (outerSequence : 'a seq) =
    query {
            for outer in outerSequence do
            join inner in innerSequence on
                (outerKeySelector outer = innerKeySelector inner)
            select (resultSelector outer inner)
    }

While this works, I'm not sure about the idiomaticity of the API.

LINQ's Join() method has this signature:

public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
    this IEnumerable<TOuter> outer,
    IEnumerable<TInner> inner,
    Func<TOuter, TKey> outerKeySelector,
    Func<TInner, TKey> innerKeySelector,
    Func<TOuter, TInner, TResult> resultSelector)

Parameters are 'logically' ordered by importance, the first being the sequence on which the method is (syntactically) called. In F#, in order to be able to pipe the 'original' sequence into the function, that argument needs to be last instead of first, which requires shuffling the whole signature around a bit.

In order to keep the two sequences next to each other, I moved innerSequence to the second to last position, and to stay in line with their order, I also switched outerKeySelector and innerKeySelector. I wonder, though, whether once I've changed it that far, it might be more idiomatic to go all the way and make it the full reverse of the "C# version", in part because now the resultSelector parameter separates the sequences from their key selector functions and looks a bit out of place in that position. What would be the most 'natural'/idiomatic way to arrange those parameters?

As for the query itself, is that the proper way to do it with regards to laziness and expression trees that will be used under the cover? Will the seq type annotations break anything in that respect? Those were necessary to actually allow for using seq and list values as inputs.

(This is of course supposed to become an extension for the Seq module; I just haven't bothered to do that yet.)

\$\endgroup\$
  • 1
    \$\begingroup\$ Why not wrap the built-in join function and re-order the params to better support pipelines? That's sort of what you've done, but the query expression seems unnecessary. \$\endgroup\$ – Daniel Aug 27 '14 at 14:27
2
\$\begingroup\$

I am porting C# code to F# that makes use of LINQ's Join() extension method. Just as I use that in method call chains, I would of course like to have an F# function to pipe into.

I think that using Join() directly in C# is actually not that common, since query syntax is much nicer for that. And I think you should consider the same thing in F#: you already know about query expressions and join, so why not use that?

Though I understand that function call chains are much more flexible, so this might not be a good option for you.


Have you considered removing the resultSelector altogether? Tuples are not very idiomatic in C#, but they are in F#, so you might want to use them.


Since you want to copy the behavior of Enumerable.Join(), I would use that to implement your method.


When considering the order of parameters, I think you should think about what is the most natural usage. I think it's this:

outer
|> Seq.join inner (fun o -> o.OKey) (fun i -> i.IKey)

This will make the order of parameters of the function quite weird, but I think it makes the most sense this way.


Putting this all together:

let join (innerSequence : 'b seq) outerKeySelector innerKeySelector (outerSequence : 'a seq) =
    outerSequence.Join(innerSequence,
        Func<_, _>(outerKeySelector), Func<_, _>(innerKeySelector),
        fun outer inner -> (outer, inner))

(Those Func casts are necessary, I think it's because F#'s weird rules for implicit casting from lambdas to delegates.)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.