So, I have 2 types like so:
pub type CSVDocument {
CSVDocument(headers: List(String), rows: List(CSVRow))
}
pub type CSVRow {
CSVRow(val: map.Map(String, String))
}
And a function like this
pub fn get_row(csv: CSVDocument, index: Int) {
list.at(csv.rows, index)
}
The return here is a Result(CSVRow, Nil), however I cannot unwrap it the way I thought it would work:
let first_row = result.unwrap(get_row(csv, 1), Nil)
This gives me the error message:
error: Type mismatch
let first_row = result.unwrap(get_row(csv, 1), Nil)
^^^
Expected type:
CSVRow
Found type:
Nil
This is my first time working with a functional language so my thought process may differ, but shouldn't this be possible?
Do I have to construct a CSVRow type with empty fields as a fallback value for unwrap?
What is the Erlang/Gleam way of doing this?
My apologies for posting this here, I'm not really a huge fan of Discord.
Thanks!
Hey @molnarmark !
The second argument to unwrap is a default value that is used if the result is an Ok, so in this case it would need to be a CSVDocument.
I think this problem might be avoidable if we use something other than the list.at function. It's quite rare to index into lists (and also expensive), it's more common to process the list as a whole using functions like map and fold.
Could you perhaps share a little more context as to what your code is trying to do? Thanks
Definitely,
What I'm working on is a CSV parser, the CSVRow type contains the map of values for the row, and the CSVDocument contains the csv headers and the rows as a list.
The code we are talking about is located here.
Coming from JavaScript my code is probably not as "functional" as it should be though.
The code looks good but I think you could rewrite this bit using the list.zip function which joins two lists into a list of tuples.
fn parse_row(headers: List(String), row: String, delimiter: String) -> CSVRow {
row
|> string.split(delimiter)
|> list.index_map(fn(i, row_val) {
let header =
headers
|> list.at(i)
tuple(result.unwrap(header, "Err"), row_val)
})
|> map.from_list
|> CSVRow
}
like this
fn parse_row(headers: List(String), row: String, delimiter: String) -> CSVRow {
row
|> string.split(delimiter)
|> list.zip(headers)
|> map.from_list
|> CSVRow
}
This is also a little bit faster as it iterates only once over the headers rather than once per column.
Coming from JavaScript my code is probably not as "functional" as it should be though.
Yeah it really helps to know all the little patterns!
Thanks, that helps.
Regarding this issue:
let first_row = result.unwrap(get_row(csv, 1), Nil)
Nil can't be the default value as the types don't match, that makes sense. Instead I solved this by returning an empty CSVRow.
let first_row = result.unwrap(get_row(csv, 1), CSVRow(map.new()))
I'd rather have it return Nil as it would indicate that the requested row is not present. Am I thinking too imperatively here?
The problem there is that Nil is not a subtype of any other type in Gleam, unlike in languages like Java etc, so you can't use it in place of a value of some other type. It might be better to think Nil as being like void or unit rather than null/nil/none/undefined/nothing.
To have a value that can be either SomeType or Nil you need to wrap those types in a custom type that can hold one or the other:
pub type IntOrNil {
ItsAnInt(Int)
ItsNil(Nil)
}
This is what Result is!
pub type Result(a, e) {
Ok(a)
Error(e)
}
pub type IntOrNil =
Result(Int, Nil)
Gleam ensures that data types are used correctly so if you have a type that is A or B it will always make you check which it is before you use it, so patterns like having things nullable are much less convenient but also much safer.
That explains it.
Thanks a lot for the help!