Skip to content

Instantly share code, notes, and snippets.

@BraedonWooding
Last active December 18, 2019 14:06
Show Gist options
  • Save BraedonWooding/2b5e9954236a41eb6e97c249ebdb6b23 to your computer and use it in GitHub Desktop.
Save BraedonWooding/2b5e9954236a41eb6e97c249ebdb6b23 to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.Linq;
public class Results
{
public double sum;
public double avg;
public double unique;
}
public static class Program
{
// O(n) but loops twice
public static Results PerformOperationsNaive(double[] array)
{
double counter = array.Sum();
return new Results() {
sum = counter,
avg = counter / array.Length,
// Count is O(1) according to docs but overall segment is O(n)
// (Surprising about count since normally Linq is O(n) count)
// hashset > hashmap (don't care about values)
unique = new HashSet<double>(array).Count()
};
}
public static Results PerformOperationsBetter(double[] array)
{
double counter = 0;
// preset capacity to improve performance
HashSet<double> set = new HashSet<double>(array.Length);
// there is a perf diff to LINQ but it's negligable
// the larger difference is the fact we are 2n
foreach (var x in array)
{
counter += x;
set.Add(x);
}
return new Results() {
sum = counter,
avg = counter / array.Length,
// Count is O(1) still
unique = set.Count()
};
}
public static Results[] GetTableResults(double[][] table, Func<double[], Results> map)
{
// trivial implementation of just a simple map
// you could parallelise this of course
return table.Select(map);
}
}
/*
If I had to write this again I would write it using decorators.
i.e. each decorator takes in 'x' processes it's change in internal state then passes it onto inner decorator
(this would be handled in abstract class to avoid annoyances / possible pitfalls of forgetting/breaking ABI)
This would have the benefit of making it simple to build the chains i.e.
GetTableResults(table, Sum.And(Avg.And(Unique.Only)))
You could use async with this approach too (allowing multiple transformations to work at once).
I sincerely doubt you would see a perf difference though (it may even be negative) because of
the fact that the work done by each object is tiny and very pipelinable and there are sync costs
(heck you may even get static polymorphism if working in C++ even with virtuals due to how nice it's setup)
You could again also parallelise the outer arrays (if you wanted to).
This would have a perf difference (positive) since each array's result is independent with the sync cost
being much smaller than the total cost of each job.
This is also much much much easier! Could even just create a worker scheduler and spawn threads for each
sub array and then sync up workers (or use async)
*/
{-# LANGUAGE ExistentialQuantification #-}
module Program where
-- Note: Set uses binary tree as backing so it's O(log n) instead of O(1)
-- Haskell hates typical hashsets/hashmaps since they aren't persistent
-- there are some packages that provide them but yeet let's avoid that
-- Other than that the complexity matches that of C#
import qualified Data.Set as S
class OperationDecorator c where
process :: c -> Double -> c
-- i.e. (Name, Value)
finalise :: c -> (String, String)
data Sum = Sum Double
data Avg = Avg Int Double
data Unique = Unique (S.Set Double)
instance OperationDecorator Sum where
process (Sum acc) = Sum . (+) acc
finalise (Sum acc) = ("sum", show acc)
instance OperationDecorator Avg where
process (Avg len acc) = Avg (len + 1) . ((+) acc)
finalise (Avg len acc) = ("avg", show $ if len > 0 then acc / fromIntegral len else 0)
instance OperationDecorator Unique where
process (Unique set) = Unique . flip S.insert set
finalise (Unique set) = ("unique", (show . S.size) set)
-- This is called existential typing
-- Basically it's just using a boxed type to get around type system requirements
-- It's a little hacky (in some ways) syntactically but often is the most
-- efficient. We can also simulate OOP but that'll cover a performance cost
-- There are other ways to fix this too (change how our data works OR work
-- on a fix set of transformations)
data AnyOperation = forall a. OperationDecorator a => AnyOperation a
instance OperationDecorator AnyOperation where
process (AnyOperation a) b = AnyOperation (process a b)
finalise (AnyOperation a) = finalise a
initSum = pack (Sum 0.0)
initAvg = pack (Avg 0 0.0)
initUnique = pack (Unique S.empty)
pack :: OperationDecorator a => a -> AnyOperation
pack = AnyOperation
applyOp :: [AnyOperation] -> [Double] -> [(String, String)]
applyOp ds [] = map finalise ds
-- This is a bit ugly... Could be cleaned up
applyOp ds (x:xs) = applyOp (map (\a -> a x) (map process ds)) xs
-- Example
results = applyOp [initSum, initAvg, initUnique] [1, 2, 3, 4, 5, 2, 2, 0, -100, 0]
-- [("sum","-81.0"),("avg","-8.1"),("unique","7")]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment