Данные, познакомьтесь с моим новым другом, F#

OSzone.net » Microsoft » Разработка приложений » Языки программирования » Данные, познакомьтесь с моим новым другом, F#
Автор: Джули Лерман
Иcточник: MSDN Magazine
Опубликована: 29.04.2014
За последние несколько лет я немного ознакомилась с функциональным программированием. Отчасти это происходило неявно: кодирование с использованием лямбд в LINQ является функциональным программированием. Но отчасти и явным образом: использование Entity Framework и типа CompiledQuery из LINQ to SQL заставило меня применять .NET-делегат Func. Это всегда было довольно хитроумно, поскольку делала я это весьма не часто. Кроме того, я знакомилась с функциональным программированием благодаря заразительному энтузиазму Рэчел Риз (Rachel Reese), Microsoft MVP, которая не только участвовала в моей местной группе пользователей (VTdotNET), но и вела группу VTFun здесь же, в Вермонте; основное внимание она уделяла многим аспектам и языкам функционального программирования. Когда я впервые пришла на заседание VTFun, все помещение было забито математиками и всякими дарованиями. Я не шучу. Одним из постоянных участников является теоретик в области конденсированных сред из Университета Вермонта. Я чуть в обморок не упала! Я слегка очумела от дискуссий по столь высоким материям, но на самом деле было довольно забавно чувствовать себя полным чайником в этой аудитории. Большая часть сказанного влетела мне в одно ухо и вылетела из другого, и лишь одно заявление привлекло мое внимание: «Функциональным языкам не нужны эти мерзкие циклы foreach». Ого! Мне захотелось узнать, что это значит. Почему это им не нужны циклы foreach?

Обычно я слышала, что функциональное программирование великолепно подходит для математических операций. Не разбираясь в математике, я воспринимала это как «для демонстраций, использующих последовательность Фибоначчи, чтобы проверить асинхронное поведение» и не обращала на это особого внимания. Это проблема того, что я слышала лишь краткие определения функционального программирования.

Но вот, наконец, я начала слышать более точные определения: функциональное программирование великолепно подходит для обработки и анализа данных (data science). Это, безусловно, привлекает специалистов в области данных. F# — функциональный язык в Microsoft .NET Framework — предоставляет все средства для обработки и анализа данных разработчикам, использующим .NET. Для него есть целые библиотеки, специально созданные для построения всевозможных диаграмм и графиков, манипуляций временем и операций над множествами (set operations). У него есть API с логикой, рассчитанной на работу с двух-, трех- и четырехмерными массивами. Он охватывает единицы измерения и способен ограничивать и проверять данные на основе выбранных единиц.

F# также делает возможными интересные сценарии кодирования в Visual Studio. Вместо того чтобы создавать логику в файлах кода, а затем отлаживать их, вы можете писать и выполнять код построчно в интерактивном окне и перемещать успешно работающий код в файл класса. Отличное представление о языке F# можно получить из статьи Томаса Петричека (Tomas Petricek) «Understanding the World with F#» (bit.ly/1cx3cGx). Добавив F# в инструментарий .NET, Visual Studio становится мощным средством для создания приложений, выполняющих логику обработки и анализа данных.

В этой статье я сосредоточусь на одном аспекте F# и функционального программирования, который я усвоила с тех пор, как однажды услышала тот комментарий по поводу циклов foreach. Функциональные языки по-настоящему хороши в работе с наборами данных. В процедурном языке, когда вы имеете дело с наборами данных, вам приходится явным образом перебирать их для выполнения той или иной логики. Функциональный язык, напротив, рассматривает наборы на другом уровне, поэтому вам остается лишь попросить его выполнить некую функцию применительно к набору, а не перебирать этот набор в цикле и выполнять ту же функцию для каждого элемента. И такая функция может быть определена с большим объемом логики, включая математическую, если в том есть необходимость.

LINQ предоставляет для этого сокращения, даже метод ForEach, которому можно передать функцию. Но в фоне ваш язык (C# или Visual Basic) просто транслирует это сокращение в соответствующий цикл.

Функциональный язык, например F#, позволяет выполнять функцию, оперирующую множеством, на более низком уровне, и это намного быстрее благодаря простоте параллельной обработки в таком языке. Добавьте к этому другие важнейшие преимущества, такие как богатые средства математической обработки и невероятно детализированную систему типов, которая понимает даже единицы измерений, и вы получите мощный инструмент для вычислений в больших наборах данных.

F# и другие функциональные языки обладают куда большими возможностями, которые лежат далеко за рамками моего понимания. Но в этой статье я хочу сосредоточиться на том, как быстро получить выгоду от одного конкретного аспекта функциональных языков без чрезмерных затрат времени на их освоение: переместить логику из базы данных в свое приложение. Это тот способ, которым я предпочитаю учиться чему-либо новому: найти несколько вещей, которые я могу понять, и использовать их для того, чтобы сделать несколько детских шажков на новой платформе, языке, инфраструктуре или в какой-то другой области.

Я слышала, как Риз пытается донести до разработчиков простую мысль о том, что использование F# не означает смены языков разработки. Тем же способом, каким можно было бы использовать LINQ-запрос или хранимую процедуру для решения конкретной задачи, можно создать библиотеку F#-методов для решения тех задач в вашем приложении, для которых гораздо лучше подходят функциональные языки.

Здесь я сконцентрируюсь на том, как извлечь бизнес-логику, встроенную в мою базу данных (логику обработки больших наборов данных, с чем превосходно справляются базы данных), и заменить ее функциональными методами.

И поскольку F# рассчитан на работу со множествами и буквально напичкан математическими функциями, этот код может оказаться более эффективным, чем в SQL или в таких процедурным языках, как C# или Visual Basic. На F# очень легко параллельно выполнять логику применительно к элементам множеств. Это не только сокращает объем кода по сравнению с тем, который понадобился бы вам в процедурном языке для эмуляции такого поведения, — распараллеливание означает, что код будет выполняться гораздо быстрее. Можно было бы написать тот же код на C# так, чтобы он выполнялся параллельно, но я, пожалуй, не стану прилагать таких усилий.

Задача из реальной жизни

Много лет назад я написала приложение на Visual Basic 5, которое должно было собирать и хранить множество научных данных, формировать отчеты по ним и выполнять уйму расчетов. Некоторые из этих расчетов были настолько сложными, что я отправляла их в Excel API.

Одно из вычислений включало определение количества фунтов на квадратный дюйм (pounds per square inch, PSI) на основе величины массы, вызывавшей разрушение материала в форме одного из цилиндров разных размеров. Приложение использовало размеры цилиндра, и в зависимости от его формы и объема вычисляло его площадь по специфической формуле. Затем применялся релевантный класс точности и, наконец, вычислялась величина массы, необходимая для разрушения цилиндра. Все вместе это давало результат в PSI для конкретного испытуемого материала.

В 1997 году применение Excel API для вычисления формулы в Visual Basic 5 и Visual Basic 6 выглядело вполне разумным решением.

Перемещение вычислений

Годы спустя я переделала это приложение под .NET. В то время я решила использовать преимущества SQL Server для выполнения расчетов PSI в крупных наборах цилиндров после обновления данных пользователем, чтобы не тратить время его компьютера на все эти вычисления. Это работало весьма прилично.

Прошло еще несколько лет, и мое отношение к бизнес-логике в базе данных переменилось. Я хотела снова переместить вычисления на клиентскую сторону, поскольку к тому времени клиентские компьютеры, конечно же, стали гораздо быстрее. Переписать всю логику на C# было не слишком трудно. После того как пользователь обновлял для серии цилиндров значения массы, потребовавшейся для их разрушения (нагрузку, выраженную в фунтах), приложение перебирало бы в цикле обновленные цилиндры и вычисляло PSI. Потом я могла бы обновить цилиндры в базе данных новыми нагрузками и значениями PSI.

Ради сравнения привычного C# с конечным результатом на F# (который вы вскоре увидите), я представила код для типа цилиндра, CylinderMeasurements, на рис. 1 и свой C#-класс калькулятора на рис. 2, чтобы вы поняли, как я получаю значения PSI для набора цилиндров. Вычисление PSI для такого набора запускаются вызовом метода CylinderCalculator.UpdateCylinders. Он перебирает в цикле каждый цилиндр в наборе и выполняет необходимые расчеты. Заметьте, что один из методов — GetAreaForCalculation — зависит от типа цилиндра, потому что я вычисляю площадь цилиндра по соответствующей формуле.

Рис. 1. Класс CylinderMeasurement

public class CylinderMeasurement
{
  public CylinderMeasurement(double widthA, double widthB,
    double height)
  {
    WidthA = widthA;
    WidthB = widthB;
    Height = height;
  }
  public int Id { get; private set; }
  public double Height { get; private set; }
  public double WidthB { get; private set; }
  public double WidthA { get; private set; }
  public int LoadPounds { get; private set; }
  public double Psi { get; set; }
  public CylinderType CylinderType { get; set; }
  public void UpdateLoadPoundsAndTypeEnum(int load,
    CylinderType cylType) {
    LoadPounds = load; CylinderType = cylType;
  }
   private double? Ratio {
    get {
      if (Height > 0 && WidthA + WidthB > 0) {
        return Math.Round(Height / ((WidthA + WidthB) / 2), 2);
      }
      return null;
    }
  }
  public double ToleranceFactor {
    get {
      if (Ratio > 1.94 || Ratio < 1) {
        return 1;
      }
      return .979;
    }
  }
}

Рис. 2. Класс калькулятора для вычисления PSI

public static class CylinderCalculator
  {
    private static CylinderMeasurement _currentCyl;
    public static void UpdateCylinders(IEnumerable<CylinderMeasurement> cyls) {
      foreach (var cyl in cyls)
      {
        _currentCyl = cyl;
        cyl.Psi = GetPsi();
      }
    }
    private static double GetPsi() {
      var area = GetAreaForCylinder();
      return PsiCalculator(area);
    }
    private static double GetAreaForCylinder() {
      switch (_currentCyl.CylinderType)
      {
        case CylinderType.FourFourEightCylinder:
          return 3.14159*((_currentCyl.WidthA + _currentCyl.WidthB)/2)/2*
            ((_currentCyl.WidthA + _currentCyl.WidthB)/2/2);
        case CylinderType.SixSixTwelveCylinder:
          return 3.14159*((_currentCyl.WidthA + _currentCyl.WidthB)/2)/2*
            ((_currentCyl.WidthA + _currentCyl.WidthB)/2/2);
        case CylinderType.ThreeThreeSixCylinder:
          return _currentCyl.WidthA*_currentCyl.WidthB;
        case CylinderType.TwoTwoTwoCylinder:
          return ((_currentCyl.WidthA + _currentCyl.WidthB)/2)*
            ((_currentCyl.WidthA + _currentCyl.WidthB)/2);
        default:
          throw new ArgumentOutOfRangeException();
      }
    }
    private static int PsiCalculator(double area) {
      if (_currentCyl.LoadPounds > 0 && area > 0)
      {
        return (int) (Math.Round(_currentCyl.LoadPounds/area/1000*
          _currentCyl.ToleranceFactor, 2)*1000);
      }
      return 0;
    }
  }

Фокусировка на данных и ускорение обработки с помощью F#

Наконец, я обнаружила, что F# благодаря своему естественному предназначению для манипуляций над данными позволяет создать гораздо более эффективное решение, не вычисляя по одной формуле за раз.

На ознакомительной сессии по F#, проводившейся Риз, я объяснила суть своей задачи, которая преследовала меня уже многие годы, и спросила, можно ли задействовать функциональный язык для более эффективного решения этой задачи. Она подтвердила, что я могла бы выполнять логику вычислений над полным набором и позволить F# получать значения PSI для множества цилиндров параллельно. Благодаря этому я могла бы заметно повысить производительность и расширить функциональность на клиентской стороне.

Главное для меня было в том, чтобы понять: F# можно использовать для решения конкретной задачи во многом так же, как я применяю хранимые процедуры, — это просто другой инструмент, который цепляется на мой пояс. Он не требует отказа от вложений в C#. Возможно, есть и такие люди, которые делали прямо противоположное: основные части своих приложений писали на F#, а C# использовали лишь для конкретных задач. В любом случае, используя C#-класс CylinderCalculator как основу, Риз создала небольшой проект на F#, который выполнял задачу, и я смогла в тестах заменить вызов своего калькулятора на вызов ее варианта, как показано на рис. 3.

Рис. 3. Калькулятор PSI на F#

module calcPsi =
  let fourFourEightFormula WA WB = 3.14159*((WA+WB)/2.)/2.*((WA+WB)/2./2.)
  let sixSixTwelveFormula WA WB = 3.14159*((WA+WB)/2.)/2.*((WA+WB)/2./2.)
  let threeThreeSixFormula (WA:float) (WB:float) = WA*WB
  let twoTwoTwoFormula WA WB = ((WA+WB)/2.)*((WA+WB)/2.)
  // Функция соотношения
  let ratioFormula height widthA widthB =
    if (height > 0. && (widthA + widthB > 0.)) then
      Some(Math.Round(height / ((widthA + widthB)/2.), 2))
    else
      None
  // Функция класса точности
  let tolerance (ratioValue:float option) = match ratioValue with
    | _ when (ratioValue.IsSome && ratioValue.Value > 1.94) -> 1.
    | _ when (ratioValue.IsSome && ratioValue.Value < 1.) -> 1.
    | _ -> 0.979
  // Обновление PSI и возврат исходной информации о цилиндрах
  let calculatePsi (cyl:CylinderMeasurement) =
    let formula = match cyl.CylinderType with
      | CylinderType.FourFourEightCylinder -> fourFourEightFormula
      | CylinderType.SixSixTwelveCylinder -> sixSixTwelveFormula
      | CylinderType.ThreeThreeSixCylinder -> threeThreeSixFormula
      | CylinderType.TwoTwoTwoCylinder -> twoTwoTwoFormula
      | _ -> failwith "Unknown cylinder"
    let basearea = formula cyl.WidthA cyl.WidthB
    let ratio = ratioFormula cyl.Height cylinder.WidthA cyl.WidthB
    let tFactor = tolerance ratio
    let PSI = Math.Round((float)cyl.LoadPounds/basearea/1000. * tFactor, 2)*1000.
    cyl.Psi <- PSI
    cyl
  // Оценка сопоставления для обработки всех заданных цилиндров
  let getPsi (cylinders:CylinderMeasurement[])
              = Array.Parallel.map calculatePsi cylinders

Если вы, как и я, новичок в F#, то, по-видимому, обратили внимание лишь на объем кода и не заметили, почему этот вариант гораздо эффективнее C#. Однако при более внимательном изучении вы, вероятно, оцените лаконизм этого языка, возможность более элегантного выражения формул и в конечном счете то, насколько легко я могу применять функцию calculatePsi, написанную Риз, к массиву цилиндров, который передается методу.

Лаконичность связана с тем, что F# лучше подходит для выполнения математических функций, чем C#, поэтому их определения более эффективны. Но меня интересовала прежде всего производительность. Когда я увеличила количество цилиндров в каждом наборе в своих тестах, изначально я не заметила никакого прироста производительности по сравнению с C#. Риз пояснила, что тестовая среда при использовании F# создает гораздо больше издержек. Тогда я проверила производительность в консольном приложении, используя Stopwatch для отчета о прошедшем времени. Приложение формировало список из 50 000 цилиндров, запускало Stopwatch, передавало цилиндры калькулятору либо на C#, либо на F#, чтобы обновить значение PSI для каждого цилиндра, а затем (по окончании вычислений) останавливало Stopwatch.

В большинстве случаев обработка на C# проходила примерно в три раза дольше, чем на F#, хотя почти в 20% случаев C# чуть-чуть обгонял F# по скорости. Я не могу объяснить причину такой странности, но, возможно, мне нужно глубже разобраться в том, как правильнее выполнять профилирование.

Заключение

Итак, хотя мне надо основательно повысить свою квалификацию в области F#, мои новые знания уже можно прекрасно применять как к приложениям, которые уже работают в производственных средах, так и к будущим приложениям. В своих коммерческих приложениях я проанализирую бизнес-логику, которую я вновь вернула в базу данных, и в каждом случае взвешу, выиграет ли конкретное приложение от замены логики на F#. В новых приложениях я буду теперь внимательнее выискивать функциональность, которую эффективнее кодировать на F#. Кроме того, всегда интересно изучать новый язык и искать в своих приложениях те сценарии, на которые и рассчитан этот язык!


Ссылка: http://www.oszone.net/23982/