Knows Not Much
Knows Not Much

Reputation: 31576

F# Pipelines access data from pipeline stages above

I have written a function like this

let GetAllDirectAssignmentsforLists (spWeb : SPWeb) =    
  spWeb.Lists 
  |> Seq.cast<SPList> 
  |> Seq.filter(fun l -> l.HasUniqueRoleAssignments) 
  |> Seq.collect (fun l -> l.RoleAssignments 
                           |> Seq.cast<SPRoleAssignment> 
                           |> Seq.map(fun ra -> ra.Member)
                 )
  |> Seq.filter (fun p -> p.GetType().Name = "SPUser")
  |> Seq.map(fun m -> m.LoginName.ToLower())

I want to return a tuple which contains the list name (taken from l.Title) in the send pipe and the m.LoginName.ToLower().

Is there a cleanway for me to get something from the above pipe elements?

One way ofcourse would be to tuple the return value in the 2nd stage of the pipe and then pass the Title all the way down.... but that would pollute the code all subsequent stages will then have to accept and return tuple values just for the sake of the last stage to get the value.

I wonder if there is a clean and easy way....

Also, in stage 4 of the pipeline (fun p -> p.GetType().Name = "SPUser") could i use if here to compare the types? rather than convert the typename to string and then match strings?

Upvotes: 2

Views: 319

Answers (2)

Tomas Petricek
Tomas Petricek

Reputation: 243126

While you can solve the problem by lifting the rest of the computation inside collect, I think that you could make the code more readable by using sequence expressions instead of pipelining.

I could not run the code to test it, but this should be equivalent:

let GetAllDirectAssignmentsforLists (spWeb : SPWeb) = seq {
  // Corresponds to your 'filter' and 'collect'
  for l in Seq.cast<SPList> spWeb.Lists do
    if l.HasUniqueRoleAssignments then
      // Corresponds to nested 'map' and 'filter'
      for ra in Seq.cast<SPRoleAssignment> l.RoleAssignments do
        let m = ra.Member
        if m.GetType().Name = "SPUser" then 
          // This implements the last 'map' operation
          yield l.Title, m.LoginName.ToLower() }

The code above corresponds more closely to the version by @pad than to your original code, because the rest of the computation is nested under for (which corresponds to nesting under collect) and so you can see all variables that are already in scope - like l which you need.

The nice thing about sequence expressions is that you can use F# constructs like if (instead of filter), for (instead of collect) etc. Also, I think it is more suitable for writing nested operations (which you need here to keep variables in scope), because it remains quite readable and keeps familiar code structure.

Upvotes: 3

pad
pad

Reputation: 41290

We exploit the fact that Seq.filter and Seq.map can be pushed inside Seq.collect without changing the results. In this case, l is still available to access.

And the last filter function is more idiomatic to use with type test operator :?.

let GetAllDirectAssignmentsforLists(spWeb: SPWeb) =    
    spWeb.Lists 
    |> Seq.cast<SPList> 
    |> Seq.filter (fun l -> l.HasUniqueRoleAssignments) 
    |> Seq.collect (fun l -> l.RoleAssignments 
                             |> Seq.cast<SPRoleAssignment> 
                             |> Seq.map (fun ra -> ra.Member)
                             |> Seq.filter (fun p -> match box p with
                                                     | :? SPUser -> true
                                                     | _ -> false)
                             |> Seq.map (fun m -> l.Title, m.LoginName.ToLower()))

To simplify further, you could change the series of Seq.map and Seq.filter to Seq.choose:

Seq.choose (fun ra -> match box ra.Member with
                      | :? SPUser -> Some (l.Title, ra.Member.LoginName.ToLower())
                      | _ -> None)

Upvotes: 4

Related Questions