Список в языке F# представляет упорядоченную, неизменяемую коллекцию элементов одного типа.
Для определения списка применяются квадратные скобки []:
let people = []
Здеь определен пустой список, который называется people
.
Также при создании списка мы можем инициализировать его элементами. Элементы помещаются внутрь квадратных скобок. Если элементы размещаются на одной строке, то они отделяется друг от друга точкой с запятой:
let people = ["Tom"; "Sam"; "Bob"]
В данном случае список people
состоит из пяти элементов типа string
. Важно, что все элементы представляют один и тот же тип.
Если элементы размещены по отдельности на разных строках, тогда не требуется разделять их точкой с запятой:
let people = [ "Tom" "Sam" "Bob" ]
Можно определить элементы списка с помощью оператора списка ..
:
let numbers = [ 1..5 ] // [1; 2; 3; 4; 5]
Можно сгенерировать элементы с помощью цикла:
let squares = [ for i in 1..5 -> i * i ] // [1; 4; 9; 16; 25]
Для создания списка из других списков можно использовать пару операторов. Оператор :: позволяет объединить одно значение со списком и создатать таким образом другой список:
let people = ["Tom"; "Sam"; "Bob"] let employees = "Alice" :: people // ["Alice"; "Tom"; "Sam"; "Bob"]
Здесь список employees создается путем объединения строки "Alice" и списка people. Причем значение указывается слева от оператора ::, а список - слева.
А для объединения двух списков применяется оператор @:
let men = ["Tom"; "Sam"] let women = ["Alice"; "Kate"] let people = men @ women // ["Tom"; "Sam"; "Alice"; "Kate"]
Для перебора элементов списка можно использовать цикл for..in:
let people = ["Tom"; "Sam"; "Bob"] for person in people do printfn "%s" person
В качестве альтернативы для перебора списка можно использовать функцию List.iter. Эта функция принимает два параметра. Первый параметр - функция, которая выполняется для каждого элемента списка. Второй параметр - сам перебираемый список:
let people = ["Tom"; "Bob"; "Alice"; "Mike"; "Sam"] List.iter (fun p -> printf "%s " p) people
Здесь функция fun p -> printf "%s " p
получает каждый элемент списка people через параметр и выводит его на консоль
Каждый элемент в списке имеет индекс - порядковый номер, который отсчитывается с нуля (то есть первый элемент имеет индекс 0, второй элемент- индекс 1 и так далее). Для обращения к элементу по индексу применяются квадратные скобки, в которые передается индекс элемента:
let people = ["Tom"; "Sam"; "Bob"] printfn "%s" people[1] // получаем элемент с индексом 1 - "Sam"
F# позволяет разложить список на отдельные значения с помощью привязки let:
let people = ["Tom"; "Bob"; "Sam"] let [a; b; c] = people printfn "%s" b // Bob
В данном случае список people раскладывается на 3 переменных a, b и c, каждая из которых будет представлять отдельный элемент списка согласно их позиции.
Если какие-то элементы не важны, то для них можно использовать символ подстановки _:
let people = ["Tom"; "Bob"; "Sam"] let [_; b;_] = people printfn "%s" b // Bob
Списки представляют класс List, который определяет ряд свойств:
Head: первый элемент
IsEmpty: возвращает true
, если в списке нет элементов, false
Item: элемент по указанному индексу.
Length: количество элементов.
Tail: список без первого элемента.
Применение:
let people = ["Tom"; "Sam"; "Bob"] printfn "Список пуст? %b" (people.IsEmpty) printfn "Количество элементов: %d" (people.Length) printfn "Первый элемент: %s" (people.Head) printfn "Второй элемент %s" (people.Tail.Head) printfn "Третий элемент %s" (people.Item(2))
Модуль List предоставляет ряд функций для выполнения различных операций над списками. Рассмотрим основные из них. В F# список представляет неизменяемую структуру данных, тем не менее для списка определен ряд функций для добавления и удаления элементов, только результат этих функций - представляет новый список (старый при этом не изменяется):
List.append(): добавляет элементы одного списка в другой
List.insertAt(): добавляет элемент по определенному индексу
List.removeAt(): удаляет элемент по определенному индексу
List.updateAt(): изменяет элемент по определенному индексу
Применение функций:
let people = ["Tom"; "Bob"] let people1 = List.append people ["Alice"; "Mike"; "Sam"] for p in people1 do printf "%s " p // Tom Bob Alice Mike Sam printfn "" let people2 = List.updateAt 0 "Tomas" people1 // по индексу 0 устанавливаем значение "Tomas" for p in people2 do printf "%s " p // Tomas Bob Alice Mike Sam printfn "" let people3 = List.removeAt 1 people2 // удаляем элемент по индексу 1 for p in people3 do printf "%s " p // Tomas Alice Mike Sam printfn "" let people4 = List.insertAt 0 "Alex" people3 // добавляем по индексу 0 строку "Alex" for p in people4 do printf "%s " p // Alex Tomas Alice Mike Sam
Ряд функций позволяют проверить соответствия элементов списка некоторому условию:
List.exists: проверяет, есть ли в списке элементы, удовлетворяют некоторому условию. Если такие имеются,
тогда возвращает true
, иначе возвращает false
.
Первый параметр функции - функция условия, а второй параметр - список для поиска
let people = ["Tom"; "Sam"; "Bob"] let result1 = List.exists (fun p -> p = "Sam") people printfn $"result1 = {result1}" // True let result2 = List.exists (fun p -> p = "Alex") people printfn $"result2 = {result2}" // False
В первом случае применяется условие fun p -> p = "Sam"
, которое проверяет значение элемента, что элемент равен строке "Sam". Во втором случае проверяем на равенство строке
"Alex". И если хотя бы один элемент с таким значением присутствует в списке, то возвращается true.
List.forall: также проверяет, есть ли в списке элементы, удовлетворяют некоторому условию, только возвращает true
, если все элементы
соответствуют этому условию
let people = ["Tom"; "Sam"; "Bob"] let result1 = List.forall (fun p -> p = "Sam") people printfn $"result1 = {result1}" // False let result2 = List.forall (fun (p:string) -> p.Length=3) people printfn $"result2 = {result2}" // True
Во втором случае проверяем, все ли элементы имеют длину в 3 символа. Поскольку обращаемся к свойству объекта string, то явным образом типизируем параметр условия типом string.
List.exists2: принимает два списка и проверяет, соответствуют ли в обоих списках элементы по одному индексу
некоторому условию. Если хотя бы одна пара элементов соответствуют, то возвращает true
, иначе возвращает false
.
Первый параметр функции - функция условия, а второй и третий параметры - сравниваемые списки
let people = ["Tom"; "Sam"; "Bob"] let employees = ["Tom"; "Alice"; "Bob"] let result1 = List.exists2 (fun p e -> p = e) people employees printfn $"result1 = {result1}" // True let students = ["Alex"; "Mike"; "Kate"] let result2 = List.exists2 (fun p s -> p = s) people students printfn $"result2 = {result2}" // False
Функция условия, которая передается в качестве первого параметра, принимает два параметра - элементы списков по одному индексу. В данном случае просто проверяем равны ли эти элементы. При этом оба списка должны иметь одинаковую длину.
List.forall2: возвращает true
, если элементы с одинаковыми индексами обоих списков соотвутствуют условию.
let people = ["Tom"; "Sam"; "Bob"] let employees = ["Tom"; "Sam"; "Bob"] let result1 = List.forall2 (fun p e -> p = e) people employees printfn $"result1 = {result1}" // True let students = ["Tom"; "Mike"; "Kate"] let result2 = List.forall2 (fun p s -> p = s) people students printfn $"result2 = {result2}" // False
Ряд функций выполняют поиск элемента в списке. Функция List.tryFind возвращает из списка первый элемент, который соответствует некоторому условию. Поскольку такого элемента может и не быть в списке,
то возвращается в реальности не сам элемент, а его обертка в виде объекта Option
. С помощью свойства Value можно получить значение.
let people = ["Tom"; "Sam"; "Bob"] let printOption option = match option with | Some value -> printfn $"{value}" | None -> printfn "Не найдено" let result1 = List.tryFind (fun (p:string) -> p.StartsWith("T")) people printOption result1 // Tom let result2 = List.tryFind (fun (p:string) -> p.EndsWith("e")) people printOption result2 // Не найдено
В первом случае ищем в списке элемент, который начинается на букву "T" и для этого применяем метод StartsWith()
. Во втором случае ищем элемент,
который заканчивается на букву "e" и для этого используем метод EndsWith()
. Оба метода определены у типа string.
Для вывода результата определяем функцию printOption, которая получает объект Option и с помощью конструкции match сопоставляет его с двумя паттернами - Some и None. Если элемент найден, тогда объект option успешно сопоставляется с Some, а его значение помещается в переменную value:
match option with | Some value -> printfn $"{value}"
Для списков также доступна функция List.find, которая также возвращает первый элемент, который соответствует по критерию. Но в отличие от
List.tryFind()
возвращается непосредственно сам элемент:
let people = ["Tom"; "Sam"; "Bob"] let result1 = List.find (fun (p:string) -> p.StartsWith("T")) people printfn $"{result1}" // Tom
Однако если в списке нет элементов, которые соответствуют критерию, то генерируется ошибка. Поэтому функция List.tryFind()
может представлять более предпочтительный способ поиска.
List.tryFindIndex: возвращает из списка индекс первого элемента, который соответствует некоторому условию.
Работает аналогично функции tryFind()
- поскольку такого элемента может и не быть в списке,
то возвращается не сам индекс элемента, а его обертка в виде объекта Option
. С помощью свойства Value можно получить значение.
let people = ["Tom"; "Sam"; "Bob"] let printOption option = match option with | Some value -> printfn $"Index: {value}" | None -> printfn "Не найдено" let result1 = List.tryFindIndex (fun (p:string) -> p.StartsWith("T")) people printOption result1 // Index: 0 let result2 = List.tryFindIndex (fun (p:string) -> p.EndsWith("e")) people printOption result2 // Не найдено
Стоит не забывать, что индексация элементов начинается с нуля.
С помощью функции List.filter можно отфильтровать элементы списка в сответствии с некоторым условием. Функция возвращает новый список, которая содержит только те элементы, которые соответствуют этому условию.
Первый параметр функции - функция условия, а второй параметр - фильтруемый список
let people = ["Tom"; "Alice"; "Sam"; "Kate"; "Bob"] let result = List.filter (fun (p:string) -> p.Length = 3) people for p in result do printf $"{p}; " // "Tom"; "Sam"; "Bob"
В данном случае получаем в новый список из исходного списка все строки, длина которых равна 3 символам.
Ряд функций выполняют сортировку элементов спика. Например, функция List.sort сортирует элементы по возрастанию, а List.sortDescending - по убыванию:
let people = ["Tom"; "Sam"; "Bob"; "Alice"; "Mike"] let sortedPeople = List.sort people // сортировка по возрастанию for p in sortedPeople do printf $"{p} " // Alice Bob Mike Sam Tom printfn "" let sortedPeopleDesc = List.sortDescending people // сортировка по убыванию for p in sortedPeopleDesc do printf $"{p} " // Tom Sam Mike Bob Alice
Стоит отметить, что сортируемые значения должны реализовать интерфейс System.IComparable, в частности, его функцию System.IComparable.CompareTo()
,
которая применяется при сравнении. Эта функция сравнивает текущий элемент с параметром и возвращает число. Если текущий элемент должен стоять перед значением из параметра,
то возвращается отрицательное значение. Если текущий элемент должен находиться после элемента из параметра, то должно возвращаться положительное значение. Например:
type Person(name:string, age:int) = member this.Name = name member this.Age = age interface System.IComparable with member this.CompareTo obj = let person = obj :?> Person // преобразуем к типу Person this.Age - person.Age // сравниваем по возрасту let people = [ Person("Tom", 39); Person("Sam", 28); Person("Bob", 43); Person("Alice", 34); Person("Mike", 31)] let sortedPeople = List.sort people for p in sortedPeople do printf $"{p.Name}({p.Age}) " // Sam(28) Mike(31) Alice(34) Tom(39) Bob(43)
Здесь реализуем функцию CompareTo
для класса Person. В эту функцию передается объект, который сравнивается с текущим. В данном случае выполняем сравнение по возрасту - свойству Age.
Причем метод получает значение типа Object, поэтому нам надо его преобразовать к типу Person, чтобы произвести сравение.
let person = obj :?> Person // преобразуем к типу Person
Затем формируем результат метода:
this.Age - person.Age // сравниваем по возрасту
То есть если возраст текущего объекта больше, чем возраст объекта, который передается через параметр, то возвращается положительное число, и поэтому текущий объект будет стоять после объекта-параметра.
Функция List.sortBy() принимает функцию, которая возвращает значение-критерий сортировки. Особенно полезна эта функция при сравнении комплексных объектов. Например:
type Person(name:string, age:int) = member this.Name = name member this.Age = age let people = [ Person("Tom", 39); Person("Sam", 28); Person("Bob", 43); Person("Alice", 34); Person("Mike", 31)] // сортировка по имени let sortedByName = List.sortBy (fun (p:Person) -> p.Name) people for p in sortedByName do printf $"{p.Name} " // Alice Bob Mike Sam Tom // сортировка по возрасту let sortedByAge = List.sortBy (fun (p:Person) -> p.Age) people for p in sortedByAge do printf $"{p.Name}({p.Age}) " // Sam(28) Mike(31) Alice(34) Tom(39) Bob(43)
Здесь сортируется список объектов Person, которые состоят из двух свойств - Name и Age. В первом случае сортируем по имени - свойству Name, поэтому для определения критерия сортировки используем функцию:
fun (p:Person) -> p.Name
В эту функцию передается объект списка, и у него выбирается значение свойства Name.
Во втором случае аналогично сортируем по имени.
Еще одна функция - List.sortWith() в качестве первого параметра принимает функцию сравнения и в качестве второго - сортируемый список:
type Person(name:string, age:int) = member this.Name = name member this.Age = age let people = [ Person("Tom", 39); Person("Sam", 28); Person("Bob", 43); Person("Alice", 34); Person("Mike", 31)] let comparePeople (person1:Person) (person2:Person) = if person1.Name > person2.Name then 1 elif person1.Name < person2.Name then -1 else 0 let sortedPeople = List.sortWith comparePeople people for p in sortedPeople do printf $"{p.Name} " // Alice Bob Mike Sam Tom
Здесь применяется функция comparePeople, которая принимает два объекта Person и сравнивает их по имени.
Для списка доступен ряд функций, которые выполняют агрегатные операции над списком:
sum: вычисляет сумму элементов
min: вычисляет минимальное значение
max: вычисляет максимальное значение
average: вычисляет среднее значение (элементы должны представлять тип float)
Применение:
let numbers = [1f; 2f; 3f; 4f; 5f] printfn "Sum: %.1f" (List.sum numbers) // Sum: 15.0 printfn "Min: %.1f" (List.min numbers) // Min: 1.0 printfn "Max: %.1f" (List.max numbers) // Max: 5.0 printfn "Average: %.1f" (List.average numbers) // Average: 3.0
Двойники этих функций с суффиксом By
в качестве первого параметра принимают функцию, которая определяет критерий для вычисления значения:
sumBy
minBy
maxBy
averageBy
countBy (вычисляет количество элементов, которые соответствуют некоторому критерию)
Например:
let people = [ ("Tom", 39); ("Bob", 43); ("Sam", 28)] let projection = fun (name, age) -> age let sum = List.sumBy projection people printfn $"Сумма возрастов: {sum}" let min = List.minBy projection people printfn $"С мин. возрастом: {min}" let max = List.maxBy projection people printfn $"С макс. возрастом: {max}" let average = List.averageBy (fun (name, age) -> float age) people printfn "Средний возраст: %.1f" average let result = List.countBy (fun (name, age) -> age > 30) people printfn $"Больше 30 лет: {result}"
Здесь имеется список people, где каждый элемент представляет кортеж из двух значений. Будем условно считать, что каждый такой кортеж представляет пользователя, где первый элемент - имя, а второй - возраст. В качестве функции-проекции, которая определяет критерий подсчета, определена функция projection:
let projection = fun (name, age) -> age
Она получает в качестве параметра - элемент списка - кортеж из двух элементов и возвращает второе значение из кортежа. То есть вычисления будем производить относительно возраста. Далее применяем эту функцию в функциях
List.sumBy/List.minBy/List.maxBy. Функция List.sumBy
возращает сумму (в данном случае сумму возрастов), List.minBy - элемент списка с минимальным значением,
а List.maxBy - элемент с максимальным значением.
List.averageBy возвращает среднее значение, но это значение вычисляется на наборе чисел float, поэтому преобразуем возраст в значение float:
let average = List.averageBy (fun (name, age) -> float age) people
List.countBy
возвращает две группы элементов - список из двух кортежей, где один кортеж содержит количество элементов, которые соответствуют
условию, а другой кортеж - сколько не соответствуют условию (в качестве условия ищем количество элементов, у которых возраст больше 30):
let result = List.countBy (fun (name, age) -> age > 30) people
Консольный вывод:
Сумма возрастов: 110 С мин. возрастом: (Sam, 28) С макс. возрастом: (Bob, 43) Средний возраст: 36.7 Больше 30 лет: [(True, 2); (False, 1)]
Ряд функций модуля List позволяют получить часть списка. Так, функции List.take и List.truncate создают новый список, который содержит определенное количество элементов исходного списка. Первый параметр функции - количество элементов, а второй параметр - список. Извлечение элементов идет с начала списка
let people = ["Tom"; "Alice"; "Bob"; "Mike"; "Sam"] let items = List.take 3 people for item in items do printf $"{item} \t" // Tom Alice Bob let items2 = List.truncate 3 people for item in items2 do printf $"{item} \t" // Tom Alice Bob
Здесь извлекаем три первых элемента. Обе функции работают похожим образом за тем исключением, что функция List.take генерирует
исключение System.InvalidOperationException
, если количество извлекаемых элементов больше количество элементов списка.
Функция List.skip пропускает определенное количество элементов с начала списка.
let people = ["Tom"; "Alice"; "Bob"; "Mike"; "Sam"] let items = List.skip 2 people // пропускаем первых 2 элемента for item in items do printf $"{item} \t" // Bob Mike Sam
Функция List.skipWhile пропускает определенное количество элементов с начала списка, которые соответствуют условию:
let people = ["Tom"; "Bob"; "Alice"; "Mike"; "Sam"] let items = List.skipWhile (fun (p:string) -> p.Length < 4) people for item in items do printf $"{item} \t" // Alice Mike Sam
Здесь пропускаем сначала строки, длина которых меньше 4 символов. Строки пропускаются только сначала, пока не дойдем до строки, которая не соответствует условию.
Функция List.takeWhile оставляет определенное количество элементов с начала списка, которые соответствуют условию:
let people = ["Tom"; "Bob"; "Alice"; "Mike"; "Sam"] let items = List.takeWhile (fun (p:string) -> p.Length < 4) people for item in items do printf $"{item} \t" // Tom Bob
Здесь добавляем в новый список строки, длина которых меньше 4 символов. Строки добавляются только сначала, пока не дойдем до строки, которая не соответствует условию.
Функция List.map позволяет преобразовать список. Первый параметр - функция преобразования, которая применяется к каждому элементу, а второй параметр - исходный список. Результат функции List.map - новый список из преобразованных элементов:
let people = [("Tom", 39); ("Bob", 43); ("Alice", 34); ("Mike", 31); ("Sam", 28)] let items = List.map (fun (name, age) -> name) people for item in items do printf $"{item} \t"
В данном случае функция преобразования fun (name, age) -> name
получает каждый элемент списка в виде кортежа из двух значений - name и age и
возвращает значение name. Соответственно из списка кортежей, где каждый элемент хранит строку и число, получим список из строк.